@aleph-alpha/chat-kit 5.7.0 → 5.9.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 (33) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-YHwE2DYx.js → CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js} +81 -52
  2. package/dist/adapters/responses-api.d.ts.map +1 -1
  3. package/dist/adapters/responses-api.js +46 -33
  4. package/dist/chat-kit.css +1 -1
  5. package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts +7 -0
  6. package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts.map +1 -1
  7. package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts +3 -1
  8. package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
  9. package/dist/components/base/CkModelSelector/CkModelSelector.stories.d.ts +6 -0
  10. package/dist/components/base/CkModelSelector/CkModelSelector.stories.d.ts.map +1 -1
  11. package/dist/components/base/CkModelSelector/CkModelSelector.vue.d.ts.map +1 -1
  12. package/dist/components/base/CkModelSelector/types.d.ts +6 -0
  13. package/dist/components/base/CkModelSelector/types.d.ts.map +1 -1
  14. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +23 -3
  15. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  16. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts +17 -2
  17. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  18. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +1 -1
  19. package/dist/components/index.js +1 -1
  20. package/dist/index.js +1 -1
  21. package/package.json +1 -1
  22. package/src/adapters/responses-api.spec.ts +6 -3
  23. package/src/adapters/responses-api.ts +62 -35
  24. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +209 -1
  25. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +84 -0
  26. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +59 -21
  27. package/src/components/base/CkModelSelector/CkModelSelector.spec.ts +23 -0
  28. package/src/components/base/CkModelSelector/CkModelSelector.stories.ts +20 -0
  29. package/src/components/base/CkModelSelector/CkModelSelector.vue +8 -1
  30. package/src/components/base/CkModelSelector/types.ts +6 -0
  31. package/src/components/composed/CkConversation/CkConversation.spec.ts +96 -5
  32. package/src/components/composed/CkConversation/CkConversation.stories.ts +328 -4
  33. package/src/components/composed/CkConversation/CkConversation.vue +38 -8
@@ -15,6 +15,13 @@ export declare const Default: Story;
15
15
  * Use when streaming tokens from an LLM into the last agent message.
16
16
  */
17
17
  export declare const StreamingSimulation: Story;
18
+ /**
19
+ * Programmatic navigation via the exposed `scrollToMessage(index)` method. Enter
20
+ * a zero-based row index and the layout scrolls that message to the top. Index
21
+ * counts all rows regardless of role (a `thread-footer`, when present, is the
22
+ * trailing row); omitting the index scrolls to the last row.
23
+ */
24
+ export declare const ScrollToMessage: Story;
18
25
  /**
19
26
  * Empty state for when there are no messages. Use as an initial state before
20
27
  * the first user turn, or when history has been cleared.
@@ -1 +1 @@
1
- {"version":3,"file":"CkConversationLayout.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.stories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAI3C,QAAA,MAAM,oBAAoB,EACoB,eAAe,CAAC;AAoB9D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,oBAAoB,CAU3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA2EnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAsBrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAiFjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAenB,CAAC"}
1
+ {"version":3,"file":"CkConversationLayout.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.stories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,KAAK,CAAC;AAI3C,QAAA,MAAM,oBAAoB,EACoB,eAAe,CAAC;AAoB9D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,oBAAoB,CAU3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA6GnC;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAsBrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAiFjC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,KA0C7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAenB,CAAC"}
@@ -7,7 +7,9 @@ declare const __VLS_export: <T extends {
7
7
  }> & (typeof globalThis extends {
8
8
  __VLS_PROPS_FALLBACK: infer P;
9
9
  } ? P : {});
10
- expose: (exposed: {}) => void;
10
+ expose: (exposed: import("vue").ShallowUnwrapRef<{
11
+ scrollToMessage: (index?: number) => void;
12
+ }>) => void;
11
13
  attrs: any;
12
14
  slots: {
13
15
  'user-message': (props: {
@@ -1 +1 @@
1
- {"version":3,"file":"CkConversationLayout.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.vue"],"names":[],"mappings":"AAyRA,QAAA,MAAM,YAAY,GAAK,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5D,aAAa,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAC9D,YAAY,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,EAC3G,gBAAgB,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAClE;WAkWO,OAAO,KAAK,EAAE,WAAW,GAAG,mBAAmB;kBA9V3C,CAAC,EAAE;MA8V2D,GAAG,CAAC,OAAO,UAAU,SAAS;QAAE,oBAAoB,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC1I,CAAC,OAAO,EAAE,EAAE,KAAK,IAAI;WACtB,GAAG;;wBA3VO,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;yBAC9B,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;QAChD;;;;WAIG;yBACc,MAAM,IAAI;;UAsVtB,EAAE;EAEL,KACQ,OAAO,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,WAAW,CAAC,CAAA;CAAI,CAAC;wBACvD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,CAAC,GAAG,EAAE,CAAC"}
1
+ {"version":3,"file":"CkConversationLayout.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkConversationLayout/CkConversationLayout.vue"],"names":[],"mappings":"AA+TA,QAAA,MAAM,YAAY,GAAK,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC5D,aAAa,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAC9D,YAAY,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC,EAC3G,gBAAgB,WAAW,CAAC,OAAO,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,EAClE;WAwYO,OAAO,KAAK,EAAE,WAAW,GAAG,mBAAmB;kBApY3C,CAAC,EAAE;MAoY2D,GAAG,CAAC,OAAO,UAAU,SAAS;QAAE,oBAAoB,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YAC1I,CAAC,OAAO,EAAE,OAAO,KAAK,EAAE,gBAAgB;kCAjPb,MAAM,KAAG,IAAI;MAiPsB,KAAK,IAAI;WACxE,GAAG;;wBAjYO,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;yBAC9B,CAAC,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC,CAAA;SAAE,KAAK,IAAI;QAChD;;;;WAIG;yBACc,MAAM,IAAI;;UA4XtB,EAAE;EAEL,KACQ,OAAO,KAAK,EAAE,KAAK,GAAG;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,WAAW,CAAC,CAAA;CAAI,CAAC;wBACvD,OAAO,YAAY;AAAxC,wBAAyC;AACzC,KAAK,mBAAmB,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAG,CAAC,GAAG,EAAE,CAAC"}
@@ -17,4 +17,10 @@ export declare const Placeholder: Story;
17
17
  * Disabled trigger — e.g. while the model list is still loading.
18
18
  */
19
19
  export declare const Disabled: Story;
20
+ /**
21
+ * Rows can carry an optional `sublabel` — a muted second line for secondary
22
+ * metadata like a serving revision or provider. It shows only in the dropdown;
23
+ * the trigger stays label-only. Rows without one render just the label.
24
+ */
25
+ export declare const WithSublabels: Story;
20
26
  //# sourceMappingURL=CkModelSelector.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CkModelSelector.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/CkModelSelector.stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,eAAe,MAAM,uBAAuB,CAAC;AASpD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,eAAe,CAyBtC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,eAAe,CAAC,CAAC;AAoC9C;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAGrB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,KAGzB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,KAGtB,CAAC"}
1
+ {"version":3,"file":"CkModelSelector.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/CkModelSelector.stories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,eAAe,MAAM,uBAAuB,CAAC;AASpD,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,eAAe,CAyBtC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,eAAe,CAAC,CAAC;AAoC9C;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,KAGrB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,KAGzB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,KAGtB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAa3B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CkModelSelector.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/CkModelSelector.vue"],"names":[],"mappings":"AAiIA,OAAO,KAAK,EAAwB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAiR1E,QAAA,MAAM,YAAY;;;;;;;;;;;6EAIhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
1
+ {"version":3,"file":"CkModelSelector.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/CkModelSelector.vue"],"names":[],"mappings":"AAwIA,OAAO,KAAK,EAAwB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAiS1E,QAAA,MAAM,YAAY;;;;;;;;;;;6EAIhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
@@ -19,6 +19,12 @@ export interface CkModelOption {
19
19
  id: string;
20
20
  /** Display name shown in the trigger and the option row. */
21
21
  label: string;
22
+ /**
23
+ * Optional secondary line shown muted beneath `label` in the dropdown row
24
+ * (e.g. a serving revision or provider). Not shown on the trigger, which
25
+ * stays label-only. Rows without one render just the label.
26
+ */
27
+ sublabel?: string;
22
28
  }
23
29
  export interface CkModelSelectorProps {
24
30
  /** Models to choose from. The dropdown trigger is still rendered when empty. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAClC,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;CACxC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/components/base/CkModelSelector/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,MAAM,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAClC,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC3C;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;CACxC"}
@@ -12,6 +12,13 @@ type Story = StoryObj<typeof meta>;
12
12
  * `#agent-message-header-content`.
13
13
  */
14
14
  export declare const Default: Story;
15
+ /**
16
+ * A diagram of how CkConversation's slots nest. Every slot has a default
17
+ * rendering; overriding a parent slot replaces everything nested beneath it,
18
+ * so the child slots below it no longer apply. Collapse a node to see which
19
+ * slots "fold away" when you take over its parent.
20
+ */
21
+ export declare const SlotAnatomy: Story;
15
22
  /**
16
23
  * Uses a remote avatar image (with fallback text). Use when you want a
17
24
  * branded agent identity in each reply.
@@ -70,11 +77,24 @@ export declare const StreamingSimulation: Story;
70
77
  */
71
78
  export declare const WithCustomMarkdownClasses: Story;
72
79
  /**
73
- * Integrates a custom code-block renderer (Shiki, Prism, etc.) via
74
- * `#agent-message-code-block`. Use when you want syntax highlighting or
75
- * inline copy buttons for assistant code.
80
+ * Fenced code blocks in agent markdown render with `UiCodeBlock` out of the
81
+ * box — syntax highlighting, a language label, and a copy button — without any
82
+ * slot wiring.
83
+ */
84
+ export declare const DefaultCodeBlock: Story;
85
+ /**
86
+ * Fenced code blocks render with `UiCodeBlock` (syntax highlighting + copy
87
+ * button) by default. Override that with a custom renderer (Prism, a bespoke
88
+ * theme, etc.) via `#agent-message-code-block`, as shown here.
76
89
  */
77
90
  export declare const WithCustomCodeBlock: Story;
91
+ /**
92
+ * Programmatic navigation via the exposed `scrollToMessage(index)` method. Enter
93
+ * a zero-based row index and the conversation scrolls that message to the top.
94
+ * Index counts all rows regardless of role (a `thread-footer`, when present, is
95
+ * the trailing row); omitting the index scrolls to the last row.
96
+ */
97
+ export declare const ScrollToMessage: Story;
78
98
  /**
79
99
  * Empty state — an initial view before the first turn. Use to show a welcome
80
100
  * screen, quick prompts, or onboarding content above this surface.
@@ -1 +1 @@
1
- {"version":3,"file":"CkConversation.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.stories.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAwI3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAkCrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA4SnC;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,KAerB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,KAYtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,KASpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,KAe5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,KAU7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAmC/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAiBnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,KAiBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KA4GjC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,KAmBvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAoDjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC"}
1
+ {"version":3,"file":"CkConversation.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.stories.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAwI3D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAkCrC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AA6WnC;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,KAerB,CAAC;AA2HF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,KAgCzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,KAYtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,KASpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,KAe5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,KAU7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAmC/B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAiBnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,KAiBpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KA4GjC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,KAmBvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,KA6C9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAoDjC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,KAsC7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,KAMnB,CAAC"}
@@ -49,7 +49,11 @@ type __VLS_Slots = {
49
49
  'agent-message-footer': (props: {
50
50
  message: BaseMessage;
51
51
  }) => void;
52
- /** Replace the default code block rendering inside agent markdown */
52
+ /**
53
+ * Replace the default code block rendering inside agent markdown. When not
54
+ * provided, fenced code blocks render with `UiCodeBlock` (syntax highlighting
55
+ * + copy button).
56
+ */
53
57
  'agent-message-code-block': (props: {
54
58
  code: string;
55
59
  language: string;
@@ -61,7 +65,18 @@ type __VLS_Slots = {
61
65
  */
62
66
  'thread-footer': () => void;
63
67
  };
64
- declare const __VLS_base: import("vue").DefineComponent<CkConversationProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<CkConversationProps> & Readonly<{}>, {
68
+ /**
69
+ * Scroll so the message at `index` sits at the top of the viewport. `index` is
70
+ * the zero-based position among the rendered rows (any `thread-footer` counts
71
+ * as the trailing row); when omitted it scrolls to the last row.
72
+ *
73
+ * This only performs the scroll — it does not focus the message. Any
74
+ * accompanying focus handling must be done separately by the caller.
75
+ */
76
+ declare function scrollToMessage(index?: number): void;
77
+ declare const __VLS_base: import("vue").DefineComponent<CkConversationProps, {
78
+ scrollToMessage: typeof scrollToMessage;
79
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<CkConversationProps> & Readonly<{}>, {
65
80
  agentAvatarImage: string;
66
81
  agentAvatarFallback: string;
67
82
  agentLoadingImage: string;
@@ -1 +1 @@
1
- {"version":3,"file":"CkConversation.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.vue"],"names":[],"mappings":"AAuPA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,gBAAgB,CAAC;AAOtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAkBnD,KAAK,WAAW,GAAG;IACjB,gDAAgD;IAChD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1D,yDAAyD;IACzD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,iDAAiD;IACjD,eAAe,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,oCAAoC;IACpC,oBAAoB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,8EAA8E;IAC9E,4BAA4B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,iFAAiF;IACjF,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,6EAA6E;IAC7E,8BAA8B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1E;;;;OAIG;IACH,+BAA+B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3E;;;;OAIG;IACH,uBAAuB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,mDAAmD;IACnD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,qEAAqE;IACrE,0BAA0B,EAAE,CAAC,KAAK,EAAE;QAClC,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,IAAI,CAAC;IACX;;;;OAIG;IACH,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AAwXF,QAAA,MAAM,UAAU;;;;6EAGd,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AAWzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"CkConversation.vue.d.ts","sourceRoot":"","sources":["../../../../src/components/composed/CkConversation/CkConversation.vue"],"names":[],"mappings":"AAqRA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,gBAAgB,CAAC;AAOtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA0BnD,KAAK,WAAW,GAAG;IACjB,gDAAgD;IAChD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1D,yDAAyD;IACzD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,iDAAiD;IACjD,eAAe,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,oCAAoC;IACpC,oBAAoB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAChE,8EAA8E;IAC9E,4BAA4B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,iFAAiF;IACjF,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE,6EAA6E;IAC7E,8BAA8B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC1E;;;;OAIG;IACH,+BAA+B,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3E;;;;OAIG;IACH,uBAAuB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,mDAAmD;IACnD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;OAIG;IACH,0BAA0B,EAAE,CAAC,KAAK,EAAE;QAClC,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,KAAK,IAAI,CAAC;IACX;;;;OAIG;IACH,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AA8DF;;;;;;;GAOG;AACH,iBAAS,eAAe,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAE7C;AAkVD,QAAA,MAAM,UAAU;;;;;;6EAId,CAAC;AACH,QAAA,MAAM,YAAY,EAAS,eAAe,CAAC,OAAO,UAAU,EAAE,WAAW,CAAC,CAAC;wBACtD,OAAO,YAAY;AAAxC,wBAAyC;AAWzC,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -40,10 +40,10 @@ declare const __VLS_base: import("vue").DefineComponent<CkHistoryProps, {}, {},
40
40
  }>, {
41
41
  showAction: boolean;
42
42
  showActionOnHover: boolean;
43
+ maxHeight: string;
43
44
  activeId: string;
44
45
  timeFormatter: (updatedAtSeconds: number) => string;
45
46
  collapsedLimit: number;
46
- maxHeight: string;
47
47
  contentClass: string;
48
48
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
49
49
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,4 +1,4 @@
1
- import { _, a, C, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z } from "../CkHistory.vue_vue_type_script_setup_true_lang-YHwE2DYx.js";
1
+ import { _, a, C, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z } from "../CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js";
2
2
  export {
3
3
  _ as CkAgentMessage,
4
4
  a as CkAgentMessageHeader,
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { u } from "./useMessageHandler-Tellov3w.js";
2
2
  import { C, d, p, u as u2, b } from "./useChatKitLabels-CMOr3Kjq.js";
3
3
  import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-Dn4ctxk8.js";
4
- import { _, a as a2, C as C2, b as b3, c as c2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A } from "./CkHistory.vue_vue_type_script_setup_true_lang-YHwE2DYx.js";
4
+ import { _, a as a2, C as C2, b as b3, c as c2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A } from "./CkHistory.vue_vue_type_script_setup_true_lang-CEqJtMuI.js";
5
5
  export {
6
6
  C as CHAT_KIT_LABELS,
7
7
  _ as CkAgentMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -456,9 +456,12 @@ describe('responsesToTree', () => {
456
456
  },
457
457
  ],
458
458
  });
459
- // The assistant turn does not inherit attachments but still carries
460
- // its responseId stamp.
461
- expect(tree.nodes['msg-a1']!.metadata).toEqual({ responseId: 'r1' });
459
+ // The assistant turn does not inherit attachments but still carries its
460
+ // responseId stamp and the response's model id.
461
+ expect(tree.nodes['msg-a1']!.metadata).toEqual({
462
+ responseId: 'r1',
463
+ model: 'pharia-1',
464
+ });
462
465
  });
463
466
 
464
467
  it('skips malformed metadata.attachments without polluting the tree', () => {
@@ -462,6 +462,53 @@ function parseResponseAttachments(
462
462
  return attachments as ResponseAttachment[];
463
463
  }
464
464
 
465
+ // Maps response-level attachments onto the shape stored in node metadata.
466
+ function toMessageAttachments(
467
+ attachments: ResponseAttachment[],
468
+ ): ResponseMessageAttachment[] {
469
+ return attachments.map(
470
+ (a): ResponseMessageAttachment => ({
471
+ name: a.fileName,
472
+ fileId: a.fileId,
473
+ fileName: a.fileName,
474
+ searchStoreId: a.searchStoreId,
475
+ ...(a.documentId ? { documentId: a.documentId } : {}),
476
+ }),
477
+ );
478
+ }
479
+
480
+ // Builds the user node for a single `request_input`, or null when the input is
481
+ // not a user turn / carries no visible content. Attachments bind to the first
482
+ // user node only (`isFirstUserNode`).
483
+ function buildUserNode(
484
+ resp: StoredResponse,
485
+ input: unknown,
486
+ idx: number,
487
+ responseAttachments: ResponseAttachment[] | null,
488
+ isFirstUserNode: boolean,
489
+ ): ResponseMessageNode | null {
490
+ const role = (input as { role?: string }).role;
491
+ const text = extractInputText((input as { content?: unknown }).content);
492
+ const hasContent = text || responseAttachments?.length;
493
+ if (role !== 'user' || !hasContent) return null;
494
+
495
+ const metadata: ResponseTreeMessageMetadata = {
496
+ responseId: resp.id,
497
+ ...(resp.model ? { model: resp.model } : {}),
498
+ };
499
+ if (responseAttachments && isFirstUserNode) {
500
+ metadata.attachments = toMessageAttachments(responseAttachments);
501
+ }
502
+
503
+ return {
504
+ id: `user-${resp.id}-${idx}`,
505
+ role: 'user',
506
+ content: [textPart(text)],
507
+ createdAt: new Date(resp.created_at * 1000),
508
+ metadata,
509
+ };
510
+ }
511
+
465
512
  // Within a single response, the visible messages chain linearly: every user
466
513
  // input is followed by exactly one assistant turn (whose reasoning, tool,
467
514
  // and text outputs are merged into one node). Across responses, branching
@@ -473,40 +520,15 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
473
520
  // the user turn that started the response. We bind them to the first
474
521
  // user `request_input` (mirroring the existing wire convention).
475
522
  const responseAttachments = parseResponseAttachments(resp);
476
- let userNodesPushed = 0;
477
- if (resp.request_input) {
478
- for (const [idx, input] of resp.request_input.entries()) {
479
- const role = (input as { role?: string }).role;
480
- const rawContent = (input as { content?: unknown }).content;
481
- const text = extractInputText(rawContent);
482
- const userContent = text || responseAttachments?.length;
483
- if (role !== 'user' || !userContent) continue;
484
-
485
- const metadata: ResponseTreeMessageMetadata = {
486
- responseId: resp.id,
487
- ...(resp.model ? { model: resp.model } : {}),
488
- };
489
- if (responseAttachments && userNodesPushed === 0) {
490
- metadata.attachments = responseAttachments.map(
491
- (a): ResponseMessageAttachment => ({
492
- name: a.fileName,
493
- fileId: a.fileId,
494
- fileName: a.fileName,
495
- searchStoreId: a.searchStoreId,
496
- ...(a.documentId ? { documentId: a.documentId } : {}),
497
- }),
498
- );
499
- }
500
-
501
- nodes.push({
502
- id: `user-${resp.id}-${idx}`,
503
- role: 'user',
504
- content: [textPart(text)],
505
- createdAt: new Date(resp.created_at * 1000),
506
- metadata,
507
- });
508
- userNodesPushed += 1;
509
- }
523
+ for (const [idx, input] of resp.request_input?.entries() ?? []) {
524
+ const userNode = buildUserNode(
525
+ resp,
526
+ input,
527
+ idx,
528
+ responseAttachments,
529
+ nodes.length === 0,
530
+ );
531
+ if (userNode) nodes.push(userNode);
510
532
  }
511
533
 
512
534
  const assistantParts = toAssistantParts(resp);
@@ -516,7 +538,12 @@ function responseToMessageNodes(resp: StoredResponse): ResponseMessageNode[] {
516
538
  role: 'assistant',
517
539
  content: assistantParts,
518
540
  createdAt: new Date((resp.completed_at ?? resp.created_at) * 1000),
519
- metadata: { responseId: resp.id },
541
+ // A stored response carries a single `model` covering both its input and
542
+ // output, so the assistant node gets the same model id as its user node.
543
+ metadata: {
544
+ responseId: resp.id,
545
+ ...(resp.model ? { model: resp.model } : {}),
546
+ },
520
547
  });
521
548
  }
522
549
  return nodes;
@@ -1,6 +1,15 @@
1
1
  import CkConversationLayout from './CkConversationLayout.vue';
2
2
  import { fireEvent, render, screen } from '@testing-library/vue';
3
- import { type Component, defineComponent, h } from 'vue';
3
+ import {
4
+ type Component,
5
+ defineComponent,
6
+ h,
7
+ nextTick,
8
+ onMounted,
9
+ ref,
10
+ } from 'vue';
11
+
12
+ type LayoutInstance = { scrollToMessage: (index?: number) => void };
4
13
 
5
14
  const messages = [
6
15
  { role: 'user', content: 'Hello there' },
@@ -37,6 +46,46 @@ function renderLayout(
37
46
  return render(Wrapper);
38
47
  }
39
48
 
49
+ /**
50
+ * Render the layout and capture the child component instance so the exposed
51
+ * `scrollToMessage` method can be invoked from the test.
52
+ */
53
+ function renderWithInstance(
54
+ msgs: Array<{ role: string; content: string }> = messages,
55
+ { withFooter = false }: { withFooter?: boolean } = {},
56
+ ) {
57
+ let instance: LayoutInstance | undefined;
58
+ // eslint-disable-next-line vue/one-component-per-file
59
+ const Wrapper = defineComponent({
60
+ setup() {
61
+ const layoutRef = ref<LayoutInstance>();
62
+ onMounted(() => {
63
+ instance = layoutRef.value;
64
+ });
65
+ return () =>
66
+ h(
67
+ CkConversationLayout as unknown as Component,
68
+ { messages: msgs, ref: layoutRef },
69
+ {
70
+ 'user-message': ({ message }: { message: (typeof msgs)[0] }) =>
71
+ h('p', { 'data-testid': 'user-msg' }, message.content),
72
+ 'agent-message': ({ message }: { message: (typeof msgs)[0] }) =>
73
+ h('p', { 'data-testid': 'agent-msg' }, message.content),
74
+ ...(withFooter
75
+ ? {
76
+ 'thread-footer': () =>
77
+ h('p', { 'data-testid': 'footer' }, 'footer'),
78
+ }
79
+ : {}),
80
+ },
81
+ );
82
+ },
83
+ });
84
+
85
+ const utils = render(Wrapper);
86
+ return { ...utils, getInstance: () => instance! };
87
+ }
88
+
40
89
  describe('CkConversationLayout', () => {
41
90
  describe('message display', () => {
42
91
  it('displays all provided messages', () => {
@@ -127,6 +176,90 @@ describe('CkConversationLayout', () => {
127
176
  });
128
177
  });
129
178
 
179
+ describe('row spacing', () => {
180
+ it('gives the first row a smaller top margin than the following rows', () => {
181
+ const { container } = renderLayout();
182
+
183
+ const rows =
184
+ container.querySelectorAll<HTMLElement>('[data-message-row]');
185
+ expect(rows[0].className).toContain('mt-2xl');
186
+ expect(rows[0].className).not.toContain('mt-3xl');
187
+ });
188
+
189
+ it('gives every non-first row the larger top margin', () => {
190
+ const { container } = renderLayout();
191
+
192
+ const rows =
193
+ container.querySelectorAll<HTMLElement>('[data-message-row]');
194
+ for (const row of Array.from(rows).slice(1)) {
195
+ expect(row.className).toContain('mt-3xl');
196
+ }
197
+ });
198
+
199
+ it('does not use a flex gap for row spacing', () => {
200
+ const { container } = renderLayout();
201
+
202
+ const scrollEl = container.querySelector(
203
+ '[class*="overflow-y-auto"]',
204
+ ) as HTMLElement;
205
+ expect(scrollEl.className).not.toContain('gap-8');
206
+ });
207
+
208
+ it('gives the thread-footer the larger top margin when messages exist', () => {
209
+ // eslint-disable-next-line vue/one-component-per-file
210
+ const Wrapper = defineComponent({
211
+ setup() {
212
+ return () =>
213
+ h(
214
+ CkConversationLayout as unknown as Component,
215
+ { messages },
216
+ {
217
+ 'user-message': () => h('p', 'user'),
218
+ 'agent-message': () => h('p', 'agent'),
219
+ 'thread-footer': () =>
220
+ h('p', { 'data-testid': 'footer' }, 'footer'),
221
+ },
222
+ );
223
+ },
224
+ });
225
+
226
+ const { container } = render(Wrapper);
227
+
228
+ const footerRow = container.querySelector(
229
+ '[data-message-row="agent"]:last-of-type',
230
+ ) as HTMLElement;
231
+ expect(footerRow.querySelector('[data-testid="footer"]')).not.toBeNull();
232
+ expect(footerRow.className).toContain('mt-3xl');
233
+ });
234
+
235
+ it('gives the thread-footer the smaller top margin when there are no messages', () => {
236
+ // eslint-disable-next-line vue/one-component-per-file
237
+ const Wrapper = defineComponent({
238
+ setup() {
239
+ return () =>
240
+ h(
241
+ CkConversationLayout as unknown as Component,
242
+ { messages: [] },
243
+ {
244
+ 'user-message': () => h('p', 'user'),
245
+ 'agent-message': () => h('p', 'agent'),
246
+ 'thread-footer': () =>
247
+ h('p', { 'data-testid': 'footer' }, 'footer'),
248
+ },
249
+ );
250
+ },
251
+ });
252
+
253
+ const { container } = render(Wrapper);
254
+
255
+ const footerRow = container.querySelector(
256
+ '[data-message-row="agent"]',
257
+ ) as HTMLElement;
258
+ expect(footerRow.className).toContain('mt-2xl');
259
+ expect(footerRow.className).not.toContain('mt-3xl');
260
+ });
261
+ });
262
+
130
263
  describe('scroll fade indicators', () => {
131
264
  function getFadeElements(container: HTMLElement) {
132
265
  return {
@@ -209,4 +342,79 @@ describe('CkConversationLayout', () => {
209
342
  expect(bottomFade.className).toContain('opacity-0');
210
343
  });
211
344
  });
345
+
346
+ describe('scrollToMessage', () => {
347
+ // jsdom reports offsetTop as 0, so stub distinct positions per rendered row.
348
+ function stubRowOffsets(container: Element) {
349
+ container
350
+ .querySelectorAll<HTMLElement>('[data-message-row]')
351
+ .forEach((row, i) => {
352
+ Object.defineProperty(row, 'offsetTop', {
353
+ configurable: true,
354
+ value: (i + 1) * 100,
355
+ });
356
+ });
357
+ }
358
+
359
+ function setup(options?: { withFooter?: boolean }) {
360
+ const { container, getInstance } = renderWithInstance(messages, options);
361
+ const scrollEl = container.querySelector(
362
+ '[class*="overflow-y-auto"]',
363
+ ) as HTMLElement;
364
+ const scrollTo = vi.fn();
365
+ scrollEl.scrollTo = scrollTo;
366
+ stubRowOffsets(container);
367
+
368
+ return { getInstance, scrollTo };
369
+ }
370
+
371
+ it('scrolls to the last message by default', async () => {
372
+ // Four rows (user, agent, user, agent) → last row is at offsetTop 400.
373
+ const { getInstance, scrollTo } = setup();
374
+
375
+ getInstance().scrollToMessage();
376
+ await nextTick();
377
+
378
+ expect(scrollTo).toHaveBeenCalledWith(
379
+ expect.objectContaining({ top: 400, behavior: 'smooth' }),
380
+ );
381
+ });
382
+
383
+ it('scrolls to the message at the given index, regardless of role', async () => {
384
+ const { getInstance, scrollTo } = setup();
385
+
386
+ getInstance().scrollToMessage(1);
387
+ await nextTick();
388
+
389
+ expect(scrollTo).toHaveBeenCalledWith(
390
+ expect.objectContaining({ top: 200, behavior: 'smooth' }),
391
+ );
392
+ });
393
+
394
+ it('treats the thread-footer as the trailing row', async () => {
395
+ // Four messages + footer → the footer is the fifth row at offsetTop 500.
396
+ const { getInstance, scrollTo } = setup({ withFooter: true });
397
+
398
+ getInstance().scrollToMessage();
399
+ await nextTick();
400
+
401
+ expect(scrollTo).toHaveBeenCalledWith(
402
+ expect.objectContaining({ top: 500, behavior: 'smooth' }),
403
+ );
404
+ });
405
+
406
+ it('does nothing when there are no messages', async () => {
407
+ const { container, getInstance } = renderWithInstance([]);
408
+ const scrollEl = container.querySelector(
409
+ '[class*="overflow-y-auto"]',
410
+ ) as HTMLElement;
411
+ const scrollTo = vi.fn();
412
+ scrollEl.scrollTo = scrollTo;
413
+
414
+ getInstance().scrollToMessage();
415
+ await nextTick();
416
+
417
+ expect(scrollTo).not.toHaveBeenCalled();
418
+ });
419
+ });
212
420
  });