@gitlab/duo-ui 15.5.0 → 15.5.1

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
@@ -1,3 +1,10 @@
1
+ ## [15.5.1](https://gitlab.com/gitlab-org/duo-ui/compare/v15.5.0...v15.5.1) (2026-01-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Fix chat autoscroll ([33b6d85](https://gitlab.com/gitlab-org/duo-ui/commit/33b6d8579a4819983b86d2557b94d599d71d1dea))
7
+
1
8
  # [15.5.0](https://gitlab.com/gitlab-org/duo-ui/compare/v15.4.2...v15.5.0) (2026-01-09)
2
9
 
3
10
 
@@ -41,6 +41,11 @@ const localeValidator = value => {
41
41
  return false;
42
42
  }
43
43
  };
44
+ const scrollWithNewContent = element => {
45
+ // Add 20px tolerance for floating point precision and unintentional micro-scrolls
46
+ // otherwise even a tiny scroll up will prevent autoscroll
47
+ return element.scrollTop + element.offsetHeight >= element.scrollHeight - 20;
48
+ };
44
49
  var script = {
45
50
  name: 'DuoChat',
46
51
  components: {
@@ -320,7 +325,7 @@ var script = {
320
325
  return {
321
326
  isHidden: false,
322
327
  prompt: '',
323
- scrolledToBottom: true,
328
+ scrollWithNewContent: true,
324
329
  activeCommandIndex: 0,
325
330
  hasValidPrompt: true,
326
331
  compositionJustEnded: false,
@@ -441,10 +446,19 @@ var script = {
441
446
  this.isHidden = false;
442
447
  },
443
448
  lastMessage(newMessage) {
444
- if (this.scrolledToBottom || (newMessage === null || newMessage === void 0 ? void 0 : newMessage.role.toLowerCase()) === MESSAGE_MODEL_ROLES.user) {
445
- // only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
446
- // or if the user has just submitted a new message
447
- this.scrollToBottom();
449
+ // Always scroll when user sends a message
450
+ if ((newMessage === null || newMessage === void 0 ? void 0 : newMessage.role.toLowerCase()) === MESSAGE_MODEL_ROLES.user) {
451
+ // Reset scrollWithNewContent so ResizeObserver will scroll during streaming
452
+ this.scrollWithNewContent = true;
453
+ this.scrollToBottomAsync();
454
+ }
455
+ },
456
+ 'lastMessage.content': {
457
+ handler() {
458
+ // Scroll when assistant message content is updated (streaming)
459
+ if (this.scrollWithNewContent) {
460
+ this.scrollToBottomAsync();
461
+ }
448
462
  }
449
463
  },
450
464
  shouldShowSlashCommands(shouldShow) {
@@ -460,7 +474,22 @@ var script = {
460
474
  this.handleScrollingThrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
461
475
  },
462
476
  mounted() {
463
- this.scrollToBottom();
477
+ var _this$$refs$chatConte;
478
+ const contentElement = (_this$$refs$chatConte = this.$refs.chatContent) === null || _this$$refs$chatConte === void 0 ? void 0 : _this$$refs$chatConte.$el;
479
+ if (contentElement) {
480
+ this.resizeObserver = new ResizeObserver(() => {
481
+ // Only scroll if user has not scrolled up (respect scroll position during streaming)
482
+ if (this.scrollWithNewContent) {
483
+ this.scrollToBottomAsync();
484
+ }
485
+ });
486
+ this.resizeObserver.observe(contentElement);
487
+ }
488
+ },
489
+ beforeUnmount() {
490
+ var _this$resizeObserver;
491
+ (_this$resizeObserver = this.resizeObserver) === null || _this$resizeObserver === void 0 ? void 0 : _this$resizeObserver.disconnect();
492
+ this.resizeObserver = null;
464
493
  },
465
494
  methods: {
466
495
  onGoBack() {
@@ -522,17 +551,18 @@ var script = {
522
551
  this.sendChatPrompt();
523
552
  },
524
553
  handleScrolling(event) {
525
- const {
526
- scrollTop,
527
- offsetHeight,
528
- scrollHeight
529
- } = event.target;
530
- this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
554
+ this.scrollWithNewContent = scrollWithNewContent(event.target);
555
+ },
556
+ scrollToBottomAsync() {
557
+ requestAnimationFrame(() => {
558
+ this.scrollToBottom();
559
+ });
531
560
  },
532
561
  async scrollToBottom() {
533
562
  var _this$$refs$anchor, _this$$refs$anchor$sc;
534
563
  await this.$nextTick();
535
564
  (_this$$refs$anchor = this.$refs.anchor) === null || _this$$refs$anchor === void 0 ? void 0 : (_this$$refs$anchor$sc = _this$$refs$anchor.scrollIntoView) === null || _this$$refs$anchor$sc === void 0 ? void 0 : _this$$refs$anchor$sc.call(_this$$refs$anchor);
565
+ this.scrollWithNewContent = true;
536
566
  },
537
567
  focusChatInput() {
538
568
  var _this$$refs$prompt, _this$$refs$prompt$$e, _this$$refs$prompt$$e2, _this$$refs$prompt$$e3;
@@ -740,7 +770,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
740
770
  },attrs:{"width":_vm.shouldRenderResizable ? _vm.dimensions.width : null,"height":_vm.shouldRenderResizable ? _vm.dimensions.height : null,"max-width":_vm.shouldRenderResizable ? _vm.dimensions.maxWidth : null,"max-height":_vm.shouldRenderResizable ? _vm.dimensions.maxHeight : null,"min-width":_vm.shouldRenderResizable ? _vm.dimensions.minWidth : null,"left":_vm.shouldRenderResizable ? _vm.dimensions.left : null,"top":_vm.shouldRenderResizable ? _vm.dimensions.top : null,"fit-parent":true,"min-height":_vm.shouldRenderResizable ? _vm.dimensions.minHeight : null,"active":_vm.shouldRenderResizable ? ['l', 't', 'lt'] : null},on:{"resize:end":_vm.updateSize}},[(!_vm.isHidden)?_c('aside',{staticClass:"duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col",class:{
741
771
  'resizable-content': _vm.shouldRenderResizable,
742
772
  'duo-chat-drawer': !_vm.shouldRenderResizable,
743
- },attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{ref:"header",attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"subtitle":_vm.activeThreadTitleForView,"error":_vm.error,"info":_vm.hasMessages ? _vm.chatState.reason : '',"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType,"session-id":_vm.sessionId,"agents":_vm.agents},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingThrottled}},[(_vm.shouldShowThreadList)?_c('duo-chat-threads',{attrs:{"threads":_vm.threadList,"preferred-locale":_vm.preferredLocale},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}}):_c('transition-group',{staticClass:"duo-chat-history gl-mt-auto gl-px-4 gl-pb-4 gl-pt-6",attrs:{"mode":"out-in","tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('duo-chat-conversation',{key:("conversation-" + index),attrs:{"enable-code-insertion":_vm.enableCodeInsertion,"messages":conversation,"show-delimiter":index > 0,"with-feedback":_vm.withFeedback,"is-tool-approval-processing":_vm.isToolApprovalProcessing,"working-directory":_vm.workingDirectory,"trusted-urls":_vm.trustedUrls},on:{"track-feedback":_vm.onTrackFeedback,"insert-code-snippet":_vm.onInsertCodeSnippet,"copy-code-snippet":_vm.onCopyCodeSnippet,"copy-message":_vm.onCopyMessage,"get-context-item-content":_vm.onGetContextItemContent,"approve-tool":_vm.onApproveToolCall,"deny-tool":_vm.onDenyToolCall,"open-file-path":_vm.onOpenFilePath}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('div',{key:"empty-state-message",staticClass:"duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-gray-900 gl-break-anywhere",attrs:{"data-testid":"gl-duo-chat-empty-state"}},[(_vm.emptyStateTitle)?_c('p',{staticClass:"gl-m-0",attrs:{"data-testid":"gl-duo-chat-empty-state-title"}},[_vm._v("\n "+_vm._s(_vm.emptyStateTitle)+"\n ")]):_vm._e(),_vm._v(" "),_c('duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})],1)]:_vm._e(),_vm._v(" "),(_vm.isLoading)?_c('duo-chat-loader',{key:"loader",attrs:{"tool-name":_vm.toolName}}):_vm._e(),_vm._v(" "),_c('div',{key:"anchor",ref:"anchor",staticClass:"scroll-anchor"})],2)],1),_vm._v(" "),(!_vm.shouldShowThreadList)?_c('footer',{staticClass:"duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0",attrs:{"data-testid":"chat-footer"}},[(_vm.$scopedSlots['footer-panel'])?_c('div',{staticClass:"gl-relative gl-max-w-full",attrs:{"data-testid":"footer-panel-wrapper"}},[_vm._t("footer-panel")],2):_vm._e(),_vm._v(" "),(_vm.$scopedSlots['footer-actions'])?_c('div',{staticClass:"gl-my-4 gl-flex gl-items-center gl-justify-between gl-gap-x-4 gl-px-4",attrs:{"data-testid":"footer-actions-wrapper"}},[_vm._t("footer-actions")],2):_vm._e(),_vm._v(" "),_c('gl-form',{attrs:{"data-testid":"chat-prompt-form"},on:{"submit":function($event){$event.stopPropagation();$event.preventDefault();return _vm.sendChatPrompt.apply(null, arguments)}}},[_c('div',{staticClass:"gl-relative gl-max-w-full"},[_vm._t("context-items-menu",null,{"isOpen":_vm.contextItemsMenuIsOpen,"onClose":_vm.closeContextItemsMenuOpen,"setRef":_vm.setContextItemsMenuRef,"focusPrompt":_vm.focusChatInput})],2),_vm._v(" "),_c('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [(_vm.canSubmit)?_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","disabled":!_vm.isChatAvailable || !_vm.chatState.isEnabled || _vm.isPromptEmpty || !_vm.hasValidPrompt,"data-testid":"chat-prompt-submit-button","aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL}}):_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full",attrs:{"icon":"stop","category":"primary","variant":"default","data-testid":"chat-prompt-cancel-button","aria-label":_vm.$options.i18n.CHAT_CANCEL_LABEL,"disabled":!_vm.canCancelInternal},on:{"click":_vm.cancelPrompt}})]},proxy:true}],null,false,4037582087)},[_c('div',{staticClass:"duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md",attrs:{"body-class":"!gl-p-2"}},_vm._l((_vm.filteredSlashCommands),function(command,index){return _c('gl-dropdown-item',{key:command.name,class:{ 'active-command': index === _vm.activeCommandIndex },on:{"click":function($event){return _vm.selectSlashCommand(index)}},nativeOn:{"mouseenter":function($event){_vm.activeCommandIndex = index;}}},[_c('span',{staticClass:"gl-flex gl-justify-between"},[_c('span',{staticClass:"gl-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-pl-3 gl-text-right gl-italic gl-text-subtle"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",attrs:{"disabled":!_vm.canSubmit || !_vm.isChatAvailable || !_vm.chatState.isEnabled,"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"character-count-limit":_vm.maxPromptLength,"textarea-classes":[
773
+ },attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('duo-chat-header',{ref:"header",attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"subtitle":_vm.activeThreadTitleForView,"error":_vm.error,"info":_vm.hasMessages ? _vm.chatState.reason : '',"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType,"session-id":_vm.sessionId,"agents":_vm.agents},on:{"go-back":_vm.onGoBack,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingThrottled}},[(_vm.shouldShowThreadList)?_c('duo-chat-threads',{attrs:{"threads":_vm.threadList,"preferred-locale":_vm.preferredLocale},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}}):_c('transition-group',{ref:"chatContent",staticClass:"duo-chat-history gl-mt-auto gl-px-4 gl-pb-4 gl-pt-6",attrs:{"mode":"out-in","tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('duo-chat-conversation',{key:("conversation-" + index),attrs:{"enable-code-insertion":_vm.enableCodeInsertion,"messages":conversation,"show-delimiter":index > 0,"with-feedback":_vm.withFeedback,"is-tool-approval-processing":_vm.isToolApprovalProcessing,"working-directory":_vm.workingDirectory,"trusted-urls":_vm.trustedUrls},on:{"track-feedback":_vm.onTrackFeedback,"insert-code-snippet":_vm.onInsertCodeSnippet,"copy-code-snippet":_vm.onCopyCodeSnippet,"copy-message":_vm.onCopyMessage,"get-context-item-content":_vm.onGetContextItemContent,"approve-tool":_vm.onApproveToolCall,"deny-tool":_vm.onDenyToolCall,"open-file-path":_vm.onOpenFilePath}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('div',{key:"empty-state-message",staticClass:"duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-gray-900 gl-break-anywhere",attrs:{"data-testid":"gl-duo-chat-empty-state"}},[(_vm.emptyStateTitle)?_c('p',{staticClass:"gl-m-0",attrs:{"data-testid":"gl-duo-chat-empty-state-title"}},[_vm._v("\n "+_vm._s(_vm.emptyStateTitle)+"\n ")]):_vm._e(),_vm._v(" "),_c('duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})],1)]:_vm._e(),_vm._v(" "),(_vm.isLoading)?_c('duo-chat-loader',{key:"loader",attrs:{"tool-name":_vm.toolName}}):_vm._e(),_vm._v(" "),_c('div',{key:"anchor",ref:"anchor",staticClass:"scroll-anchor"})],2)],1),_vm._v(" "),(!_vm.shouldShowThreadList)?_c('footer',{staticClass:"duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0",attrs:{"data-testid":"chat-footer"}},[(_vm.$scopedSlots['footer-panel'])?_c('div',{staticClass:"gl-relative gl-max-w-full",attrs:{"data-testid":"footer-panel-wrapper"}},[_vm._t("footer-panel")],2):_vm._e(),_vm._v(" "),(_vm.$scopedSlots['footer-actions'])?_c('div',{staticClass:"gl-my-4 gl-flex gl-items-center gl-justify-between gl-gap-x-4 gl-px-4",attrs:{"data-testid":"footer-actions-wrapper"}},[_vm._t("footer-actions")],2):_vm._e(),_vm._v(" "),_c('gl-form',{attrs:{"data-testid":"chat-prompt-form"},on:{"submit":function($event){$event.stopPropagation();$event.preventDefault();return _vm.sendChatPrompt.apply(null, arguments)}}},[_c('div',{staticClass:"gl-relative gl-max-w-full"},[_vm._t("context-items-menu",null,{"isOpen":_vm.contextItemsMenuIsOpen,"onClose":_vm.closeContextItemsMenuOpen,"setRef":_vm.setContextItemsMenuRef,"focusPrompt":_vm.focusChatInput})],2),_vm._v(" "),_c('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [(_vm.canSubmit)?_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","disabled":!_vm.isChatAvailable || !_vm.chatState.isEnabled || _vm.isPromptEmpty || !_vm.hasValidPrompt,"data-testid":"chat-prompt-submit-button","aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL}}):_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full",attrs:{"icon":"stop","category":"primary","variant":"default","data-testid":"chat-prompt-cancel-button","aria-label":_vm.$options.i18n.CHAT_CANCEL_LABEL,"disabled":!_vm.canCancelInternal},on:{"click":_vm.cancelPrompt}})]},proxy:true}],null,false,4037582087)},[_c('div',{staticClass:"duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md",attrs:{"body-class":"!gl-p-2"}},_vm._l((_vm.filteredSlashCommands),function(command,index){return _c('gl-dropdown-item',{key:command.name,class:{ 'active-command': index === _vm.activeCommandIndex },on:{"click":function($event){return _vm.selectSlashCommand(index)}},nativeOn:{"mouseenter":function($event){_vm.activeCommandIndex = index;}}},[_c('span',{staticClass:"gl-flex gl-justify-between"},[_c('span',{staticClass:"gl-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-pl-3 gl-text-right gl-italic gl-text-subtle"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",attrs:{"disabled":!_vm.canSubmit || !_vm.isChatAvailable || !_vm.chatState.isEnabled,"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"character-count-limit":_vm.maxPromptLength,"textarea-classes":[
744
774
  'gl-absolute',
745
775
  '!gl-h-full',
746
776
  'gl-rounded-br-none',
@@ -788,4 +818,4 @@ var __vue_staticRenderFns__ = [];
788
818
  );
789
819
 
790
820
  export default __vue_component__;
791
- export { i18n };
821
+ export { i18n, scrollWithNewContent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/duo-ui",
3
- "version": "15.5.0",
3
+ "version": "15.5.1",
4
4
  "description": "Duo UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -98,6 +98,12 @@ const localeValidator = (value) => {
98
98
  }
99
99
  };
100
100
 
101
+ export const scrollWithNewContent = (element) => {
102
+ // Add 20px tolerance for floating point precision and unintentional micro-scrolls
103
+ // otherwise even a tiny scroll up will prevent autoscroll
104
+ return element.scrollTop + element.offsetHeight >= element.scrollHeight - 20;
105
+ };
106
+
101
107
  export default {
102
108
  name: 'DuoChat',
103
109
  components: {
@@ -377,7 +383,7 @@ export default {
377
383
  return {
378
384
  isHidden: false,
379
385
  prompt: '',
380
- scrolledToBottom: true,
386
+ scrollWithNewContent: true,
381
387
  activeCommandIndex: 0,
382
388
  hasValidPrompt: true,
383
389
  compositionJustEnded: false,
@@ -520,12 +526,21 @@ export default {
520
526
  this.isHidden = false;
521
527
  },
522
528
  lastMessage(newMessage) {
523
- if (this.scrolledToBottom || newMessage?.role.toLowerCase() === MESSAGE_MODEL_ROLES.user) {
524
- // only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
525
- // or if the user has just submitted a new message
526
- this.scrollToBottom();
529
+ // Always scroll when user sends a message
530
+ if (newMessage?.role.toLowerCase() === MESSAGE_MODEL_ROLES.user) {
531
+ // Reset scrollWithNewContent so ResizeObserver will scroll during streaming
532
+ this.scrollWithNewContent = true;
533
+ this.scrollToBottomAsync();
527
534
  }
528
535
  },
536
+ 'lastMessage.content': {
537
+ handler() {
538
+ // Scroll when assistant message content is updated (streaming)
539
+ if (this.scrollWithNewContent) {
540
+ this.scrollToBottomAsync();
541
+ }
542
+ },
543
+ },
529
544
  shouldShowSlashCommands(shouldShow) {
530
545
  if (shouldShow) {
531
546
  this.onShowSlashCommands();
@@ -539,7 +554,20 @@ export default {
539
554
  this.handleScrollingThrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
540
555
  },
541
556
  mounted() {
542
- this.scrollToBottom();
557
+ const contentElement = this.$refs.chatContent?.$el;
558
+ if (contentElement) {
559
+ this.resizeObserver = new ResizeObserver(() => {
560
+ // Only scroll if user has not scrolled up (respect scroll position during streaming)
561
+ if (this.scrollWithNewContent) {
562
+ this.scrollToBottomAsync();
563
+ }
564
+ });
565
+ this.resizeObserver.observe(contentElement);
566
+ }
567
+ },
568
+ beforeUnmount() {
569
+ this.resizeObserver?.disconnect();
570
+ this.resizeObserver = null;
543
571
  },
544
572
 
545
573
  methods: {
@@ -605,13 +633,18 @@ export default {
605
633
  this.sendChatPrompt();
606
634
  },
607
635
  handleScrolling(event) {
608
- const { scrollTop, offsetHeight, scrollHeight } = event.target;
609
- this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
636
+ this.scrollWithNewContent = scrollWithNewContent(event.target);
637
+ },
638
+ scrollToBottomAsync() {
639
+ requestAnimationFrame(() => {
640
+ this.scrollToBottom();
641
+ });
610
642
  },
611
643
  async scrollToBottom() {
612
644
  await this.$nextTick();
613
645
 
614
646
  this.$refs.anchor?.scrollIntoView?.();
647
+ this.scrollWithNewContent = true;
615
648
  },
616
649
  focusChatInput() {
617
650
  this.$refs.prompt?.$el?.querySelector?.('textarea')?.focus();
@@ -885,6 +918,7 @@ export default {
885
918
  />
886
919
  <transition-group
887
920
  v-else
921
+ ref="chatContent"
888
922
  mode="out-in"
889
923
  tag="section"
890
924
  name="message"