@gitlab/duo-ui 15.0.4 → 15.0.6

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,19 @@
1
+ ## [15.0.6](https://gitlab.com/gitlab-org/duo-ui/compare/v15.0.5...v15.0.6) (2025-12-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * trustedUrls propagation to renderMarkdown ([7e296d1](https://gitlab.com/gitlab-org/duo-ui/commit/7e296d15d82d9edc5f3216c1f557861d6ba37272))
7
+ * trustedUrls validation and comments ([66b1307](https://gitlab.com/gitlab-org/duo-ui/commit/66b1307938f07d50792fcf3d95337512846855fc))
8
+
9
+ ## [15.0.5](https://gitlab.com/gitlab-org/duo-ui/compare/v15.0.4...v15.0.5) (2025-12-07)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * Do not allow to cancel chat while tool is being processed ([41ba989](https://gitlab.com/gitlab-org/duo-ui/commit/41ba9896584762a5d0064d64a6284dd563494b46))
15
+ * Tool approval buttons enabled too fast ([8977579](https://gitlab.com/gitlab-org/duo-ui/commit/897757973ad54ac69a6bfb225df4c88bcda6426b))
16
+
1
17
  ## [15.0.4](https://gitlab.com/gitlab-org/duo-ui/compare/v15.0.3...v15.0.4) (2025-12-03)
2
18
 
3
19
 
@@ -284,6 +284,17 @@ var script = {
284
284
  type: String,
285
285
  required: false,
286
286
  default: () => ''
287
+ },
288
+ /**
289
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
290
+ * that are allowed to render as clickable links in markdown content.
291
+ * Links to other domains will not be clickable.
292
+ */
293
+ trustedUrls: {
294
+ type: Array,
295
+ required: false,
296
+ default: () => [],
297
+ validator: urls => urls.every(url => typeof url === 'string')
287
298
  }
288
299
  },
289
300
  data() {
@@ -305,11 +316,7 @@ var script = {
305
316
  computed: {
306
317
  canSubmit() {
307
318
  const shouldAllowSubmit = !this.isLoading && !this.isStreaming && !this.isToolApprovalProcessing && !this.isAwaitingToolApproval;
308
-
309
- // Fallback logic: If we're not loading, not streaming, and not awaiting approval,
310
- // but isToolApprovalProcessing is stuck at true, we should still enable submit
311
- const isStuckInProcessing = !this.isLoading && !this.isStreaming && !this.isAwaitingToolApproval && this.isToolApprovalProcessing;
312
- return shouldAllowSubmit || isStuckInProcessing;
319
+ return shouldAllowSubmit;
313
320
  },
314
321
  shouldShowThreadList() {
315
322
  return this.isMultithreaded && this.currentView === VIEW_TYPES.LIST;
@@ -398,6 +405,13 @@ var script = {
398
405
  return Boolean(
399
406
  // It is possible to get tool null, so we assert it is truthy to make sure there is a payload
400
407
  lastMessage && lastMessage.message_type === 'request' && lastMessage.tool_info);
408
+ },
409
+ canCancelInternal() {
410
+ // Don't allow cancel while there are pending tool approvals
411
+ if (this.isAwaitingToolApproval) {
412
+ return false;
413
+ }
414
+ return this.canCancel;
401
415
  }
402
416
  },
403
417
  watch: {
@@ -707,7 +721,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
707
721
  },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:"markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col",class:{
708
722
  'resizable-content': _vm.shouldRenderResizable,
709
723
  'duo-chat-drawer': !_vm.shouldRenderResizable,
710
- },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,"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},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.isChatAvailable && !_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.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.canCancel},on:{"click":_vm.cancelPrompt}})]},proxy:true}],null,false,2657534213)},[_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,"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"character-count-limit":_vm.maxPromptLength,"textarea-classes":[
724
+ },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,"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.isChatAvailable && !_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.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,3149367298)},[_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,"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"character-count-limit":_vm.maxPromptLength,"textarea-classes":[
711
725
  'gl-absolute',
712
726
  '!gl-h-full',
713
727
  'gl-rounded-br-none',
@@ -317,6 +317,17 @@ var script = {
317
317
  type: Boolean,
318
318
  required: false,
319
319
  default: true
320
+ },
321
+ /**
322
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
323
+ * that are allowed to render as clickable links in markdown content.
324
+ * Links to other domains will not be clickable.
325
+ */
326
+ trustedUrls: {
327
+ type: Array,
328
+ required: false,
329
+ default: () => [],
330
+ validator: urls => urls.every(url => typeof url === 'string')
320
331
  }
321
332
  },
322
333
  data() {
@@ -748,7 +759,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
748
759
  'duo-chat-history gl-px-4',
749
760
  _vm.$scopedSlots['custom-empty-state']
750
761
  ? 'gl-flex gl-flex-1 gl-items-center gl-justify-center'
751
- : 'gl-mt-auto gl-pb-4 gl-pt-6' ],attrs:{"mode":"out-in","tag":"section","name":"message","data-testid":"chat-messages"}},[_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},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)?[_vm._t("custom-empty-state",function(){return [_c('div',{key:"empty-state-message",staticClass:"duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-default gl-break-anywhere",attrs:{"data-testid":"gl-duo-chat-empty-state"}},[_c('div',{staticClass:"gl-mb-[3.75rem] gl-flex gl-flex-col gl-items-center gl-justify-center gl-gap-3 gl-text-center"},[_c('h1',{staticClass:"gl-my-0 gl-text-[3.5rem]",attrs:{"data-testid":"gl-duo-chat-empty-state-emoji"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_EMPTY_STATE_EMOJI)+"\n ")]),_vm._v(" "),(_vm.agentName)?_c('h2',{staticClass:"gl-heading-2 gl-my-0",attrs:{"data-testid":"gl-duo-chat-empty-state-greeting"}},[_vm._v("\n "+_vm._s(_vm.emptyStateGreeting)+"\n ")]):_vm._e(),_vm._v(" "),_c('h2',{staticClass:"gl-my-0 gl-text-size-h2",attrs:{"data-testid":"gl-duo-chat-empty-state-title"}},[_vm._v("\n "+_vm._s(_vm.emptyStateMainText)+"\n ")]),_vm._v(" "),_c('p',{staticClass:"gl-text-base gl-text-subtle",attrs:{"data-testid":"gl-duo-chat-empty-state-subtitle"}},[_vm._v("\n "+_vm._s(_vm.emptyStateSubText)+"\n ")])]),_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"}},[_c('p',{staticClass:"gl-mb-3 gl-text-sm gl-text-subtle",class:{ 'gl-invisible': !_vm.hasAssistantMessages },attrs:{"data-testid":"chat-disclaimer"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_DISCLAIMER)+"\n ")]),_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('div',{staticClass:"duo-chat-input gl-relative gl-min-h-8 gl-max-w-full gl-grow gl-flex-col gl-rounded-bl-[12px] gl-rounded-br-[18px] gl-rounded-tl-[12px] gl-rounded-tr-[12px] gl-align-top forced-colors:gl-border"},[(_vm.$scopedSlots['agentic-model'] || _vm.$scopedSlots['agentic-switch'])?_c('div',{staticClass:"gl-flex gl-items-center gl-justify-between gl-gap-5 gl-border-0 gl-border-b-1 gl-border-solid gl-border-strong gl-px-4 gl-py-4 forced-colors:gl-border-none"},[_c('div',{staticClass:"duo-model-switcher gl-min-w-0 gl-max-w-full"},[_vm._t("agentic-model")],2),_vm._v(" "),_c('div',{staticClass:"duo-agent-mode-switcher gl-min-w-0 gl-max-w-full gl-shrink-0"},[_vm._t("agentic-switch")],2)]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-h-[80px] gl-grow",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":[
762
+ : 'gl-mt-auto gl-pb-4 gl-pt-6' ],attrs:{"mode":"out-in","tag":"section","name":"message","data-testid":"chat-messages"}},[_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)?[_vm._t("custom-empty-state",function(){return [_c('div',{key:"empty-state-message",staticClass:"duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-default gl-break-anywhere",attrs:{"data-testid":"gl-duo-chat-empty-state"}},[_c('div',{staticClass:"gl-mb-[3.75rem] gl-flex gl-flex-col gl-items-center gl-justify-center gl-gap-3 gl-text-center"},[_c('h1',{staticClass:"gl-my-0 gl-text-[3.5rem]",attrs:{"data-testid":"gl-duo-chat-empty-state-emoji"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_EMPTY_STATE_EMOJI)+"\n ")]),_vm._v(" "),(_vm.agentName)?_c('h2',{staticClass:"gl-heading-2 gl-my-0",attrs:{"data-testid":"gl-duo-chat-empty-state-greeting"}},[_vm._v("\n "+_vm._s(_vm.emptyStateGreeting)+"\n ")]):_vm._e(),_vm._v(" "),_c('h2',{staticClass:"gl-my-0 gl-text-size-h2",attrs:{"data-testid":"gl-duo-chat-empty-state-title"}},[_vm._v("\n "+_vm._s(_vm.emptyStateMainText)+"\n ")]),_vm._v(" "),_c('p',{staticClass:"gl-text-base gl-text-subtle",attrs:{"data-testid":"gl-duo-chat-empty-state-subtitle"}},[_vm._v("\n "+_vm._s(_vm.emptyStateSubText)+"\n ")])]),_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"}},[_c('p',{staticClass:"gl-mb-3 gl-text-sm gl-text-subtle",class:{ 'gl-invisible': !_vm.hasAssistantMessages },attrs:{"data-testid":"chat-disclaimer"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_DISCLAIMER)+"\n ")]),_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('div',{staticClass:"duo-chat-input gl-relative gl-min-h-8 gl-max-w-full gl-grow gl-flex-col gl-rounded-bl-[12px] gl-rounded-br-[18px] gl-rounded-tl-[12px] gl-rounded-tr-[12px] gl-align-top forced-colors:gl-border"},[(_vm.$scopedSlots['agentic-model'] || _vm.$scopedSlots['agentic-switch'])?_c('div',{staticClass:"gl-flex gl-items-center gl-justify-between gl-gap-5 gl-border-0 gl-border-b-1 gl-border-solid gl-border-strong gl-px-4 gl-py-4 forced-colors:gl-border-none"},[_c('div',{staticClass:"duo-model-switcher gl-min-w-0 gl-max-w-full"},[_vm._t("agentic-model")],2),_vm._v(" "),_c('div',{staticClass:"duo-agent-mode-switcher gl-min-w-0 gl-max-w-full gl-shrink-0"},[_vm._t("agentic-switch")],2)]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-h-[80px] gl-grow",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":[
752
763
  '!gl-h-full',
753
764
  '!gl-bg-transparent',
754
765
  '!gl-py-4',
@@ -47,12 +47,15 @@ var script = {
47
47
  default: true
48
48
  },
49
49
  /**
50
- * Base URL of the GitLab instance.
50
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
51
+ * that are allowed to render as clickable links in markdown content.
52
+ * Links to other domains will not be clickable.
51
53
  */
52
54
  trustedUrls: {
53
55
  type: Array,
54
56
  required: false,
55
- default: () => []
57
+ default: () => [],
58
+ validator: urls => urls.every(url => typeof url === 'string')
56
59
  },
57
60
  /**
58
61
  * Whether the chat should show the feedback link on the assistant messages.
@@ -67,12 +67,15 @@ var script = {
67
67
  required: true
68
68
  },
69
69
  /**
70
- * Base URL of the GitLab instance.
70
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
71
+ * that are allowed to render as clickable links in markdown content.
72
+ * Links to other domains will not be clickable.
71
73
  */
72
74
  trustedUrls: {
73
75
  type: Array,
74
76
  required: false,
75
- default: () => []
77
+ default: () => [],
78
+ validator: urls => urls.every(url => typeof url === 'string')
76
79
  },
77
80
  /**
78
81
  * Whether the chat should show the feedback link on the assistant messages.
@@ -136,17 +139,27 @@ var script = {
136
139
  if (this.message.contentHtml) {
137
140
  return this.message.contentHtml;
138
141
  }
139
- return this.renderMarkdown(this.message.content, this.trustedUrls);
142
+ return this.renderMarkdown(this.message.content, {
143
+ trustedUrls: this.trustedUrls
144
+ });
140
145
  },
141
146
  messageContent() {
142
147
  if (this.isAssistantMessage && this.isChunk) {
143
- return this.renderMarkdown(concatUntilEmpty(this.messageChunks), this.trustedUrls);
148
+ return this.renderMarkdown(concatUntilEmpty(this.messageChunks), {
149
+ trustedUrls: this.trustedUrls
150
+ });
144
151
  }
145
- return this.renderMarkdown(this.defaultContent, this.trustedUrls) || this.renderMarkdown(concatUntilEmpty(this.message.chunks), this.trustedUrls);
152
+ return this.renderMarkdown(this.defaultContent, {
153
+ trustedUrls: this.trustedUrls
154
+ }) || this.renderMarkdown(concatUntilEmpty(this.message.chunks), {
155
+ trustedUrls: this.trustedUrls
156
+ });
146
157
  },
147
158
  renderedError() {
148
159
  var _this$message$errors;
149
- return this.renderMarkdown(((_this$message$errors = this.message.errors) === null || _this$message$errors === void 0 ? void 0 : _this$message$errors.join('; ')) || '', this.trustedUrls);
160
+ return this.renderMarkdown(((_this$message$errors = this.message.errors) === null || _this$message$errors === void 0 ? void 0 : _this$message$errors.join('; ')) || '', {
161
+ trustedUrls: this.trustedUrls
162
+ });
150
163
  },
151
164
  error() {
152
165
  var _this$message, _this$message$errors2;
@@ -165,13 +165,14 @@ var script = {
165
165
  return this.approvalStatus === TOOL_STATUS.Approved;
166
166
  },
167
167
  isApproving() {
168
- return this.isProcessing && this.localProcessingState === PROCESSING_STATE.APPROVING;
168
+ return this.localProcessingState === PROCESSING_STATE.APPROVING || this.isProcessing;
169
169
  },
170
170
  isDenying() {
171
- return this.isProcessing && this.localProcessingState === PROCESSING_STATE.DENYING;
171
+ return this.localProcessingState === PROCESSING_STATE.DENYING;
172
172
  },
173
173
  buttonsDisabled() {
174
- return this.isProcessing;
174
+ // Disable if we've taken action locally OR if parent says we're processing
175
+ return this.localProcessingState !== PROCESSING_STATE.NONE || this.isProcessing;
175
176
  },
176
177
  approveButtonText() {
177
178
  return this.isApproving ? this.$options.i18n.APPROVING_TEXT : this.$options.i18n.APPROVE_TEXT;
@@ -197,51 +198,45 @@ var script = {
197
198
  }
198
199
  },
199
200
  watch: {
200
- // Reset local state when processing completes
201
- isProcessing(newVal, oldVal) {
202
- if (oldVal === true && newVal === false) {
203
- this.resetProcessingState();
201
+ approvalStatus(newVal) {
202
+ if (newVal === TOOL_STATUS.Approved) {
203
+ this.collapsed = true;
204
204
  }
205
205
  }
206
206
  },
207
207
  methods: {
208
- handleApprove() {
209
- if (this.isProcessing) return;
210
- this.localProcessingState = PROCESSING_STATE.APPROVING;
211
- // Default to primary option type for backward compatibility
212
- const approvalPayload = {
213
- type: this.primaryApprovalOption.type || acceptedApproveToolPayloads.APPROVE_TOOL_ONCE
214
- };
215
- this.$emit('approve-tool', approvalPayload);
216
- },
217
- handlePrimaryApprove() {
218
- if (this.isProcessing) return;
208
+ approveWithType(approvalType) {
209
+ if (this.localProcessingState !== PROCESSING_STATE.NONE) {
210
+ return;
211
+ }
219
212
  this.localProcessingState = PROCESSING_STATE.APPROVING;
220
213
  this.$emit('approve-tool', {
221
- type: this.primaryApprovalOption.type
214
+ type: approvalType
222
215
  });
223
216
  },
217
+ handleApprove() {
218
+ const approvalType = this.primaryApprovalOption.type || acceptedApproveToolPayloads.APPROVE_TOOL_ONCE;
219
+ this.approveWithType(approvalType);
220
+ },
221
+ handlePrimaryApprove() {
222
+ this.approveWithType(this.primaryApprovalOption.type);
223
+ },
224
224
  handleDropdownSelection(option) {
225
- if (this.isProcessing) return;
226
- this.localProcessingState = PROCESSING_STATE.APPROVING;
227
- this.$emit('approve-tool', {
228
- type: option.type
229
- });
225
+ this.approveWithType(option.type);
230
226
  },
231
227
  handleDeny() {
232
- if (this.isProcessing) return;
233
228
  this.showDenialReason = true;
234
229
  },
235
230
  cancelDenial() {
236
231
  this.showDenialReason = false;
237
232
  this.denialReason = '';
238
- this.localProcessingState = PROCESSING_STATE.NONE;
239
233
  },
240
234
  submitDenial() {
241
- if (this.isProcessing) return;
235
+ if (this.localProcessingState !== PROCESSING_STATE.NONE) {
236
+ return;
237
+ }
242
238
  this.localProcessingState = PROCESSING_STATE.DENYING;
243
239
  this.$emit('deny-tool', this.denialReason || null);
244
- // Don't reset state here - wait for isProcessing to change
245
240
  },
246
241
  resetProcessingState() {
247
242
  this.localProcessingState = PROCESSING_STATE.NONE;
@@ -242,12 +242,15 @@ var script = {
242
242
  default: false
243
243
  },
244
244
  /**
245
- * Base URL of the GitLab instance.
245
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
246
+ * that are allowed to render as clickable links in markdown content.
247
+ * Links to other domains will not be clickable.
246
248
  */
247
249
  trustedUrls: {
248
250
  type: Array,
249
251
  required: false,
250
- default: () => []
252
+ default: () => [],
253
+ validator: urls => urls.every(url => typeof url === 'string')
251
254
  },
252
255
  /*
253
256
  * The preferred locale for the chat interface.
@@ -219,12 +219,15 @@ var script = {
219
219
  default: false
220
220
  },
221
221
  /**
222
- * Base URL of the GitLab instance.
222
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
223
+ * that are allowed to render as clickable links in markdown content.
224
+ * Links to other domains will not be clickable.
223
225
  */
224
226
  trustedUrls: {
225
227
  type: Array,
226
228
  required: false,
227
- default: () => []
229
+ default: () => [],
230
+ validator: urls => urls.every(url => typeof url === 'string')
228
231
  },
229
232
  /*
230
233
  * The preferred locale for the chat interface.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/duo-ui",
3
- "version": "15.0.4",
3
+ "version": "15.0.6",
4
4
  "description": "Duo UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -345,6 +345,17 @@ export default {
345
345
  required: false,
346
346
  default: () => '',
347
347
  },
348
+ /**
349
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
350
+ * that are allowed to render as clickable links in markdown content.
351
+ * Links to other domains will not be clickable.
352
+ */
353
+ trustedUrls: {
354
+ type: Array,
355
+ required: false,
356
+ default: () => [],
357
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
358
+ },
348
359
  },
349
360
  data() {
350
361
  return {
@@ -370,15 +381,7 @@ export default {
370
381
  !this.isToolApprovalProcessing &&
371
382
  !this.isAwaitingToolApproval;
372
383
 
373
- // Fallback logic: If we're not loading, not streaming, and not awaiting approval,
374
- // but isToolApprovalProcessing is stuck at true, we should still enable submit
375
- const isStuckInProcessing =
376
- !this.isLoading &&
377
- !this.isStreaming &&
378
- !this.isAwaitingToolApproval &&
379
- this.isToolApprovalProcessing;
380
-
381
- return shouldAllowSubmit || isStuckInProcessing;
384
+ return shouldAllowSubmit;
382
385
  },
383
386
  shouldShowThreadList() {
384
387
  return this.isMultithreaded && this.currentView === VIEW_TYPES.LIST;
@@ -484,6 +487,14 @@ export default {
484
487
  lastMessage && lastMessage.message_type === 'request' && lastMessage.tool_info
485
488
  );
486
489
  },
490
+ canCancelInternal() {
491
+ // Don't allow cancel while there are pending tool approvals
492
+ if (this.isAwaitingToolApproval) {
493
+ return false;
494
+ }
495
+
496
+ return this.canCancel;
497
+ },
487
498
  },
488
499
  watch: {
489
500
  multiThreadedView(newView) {
@@ -871,6 +882,7 @@ export default {
871
882
  :with-feedback="withFeedback"
872
883
  :is-tool-approval-processing="isToolApprovalProcessing"
873
884
  :working-directory="workingDirectory"
885
+ :trusted-urls="trustedUrls"
874
886
  @track-feedback="onTrackFeedback"
875
887
  @insert-code-snippet="onInsertCodeSnippet"
876
888
  @copy-code-snippet="onCopyCodeSnippet"
@@ -1029,7 +1041,7 @@ export default {
1029
1041
  class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
1030
1042
  data-testid="chat-prompt-cancel-button"
1031
1043
  :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
1032
- :disabled="!canCancel"
1044
+ :disabled="!canCancelInternal"
1033
1045
  @click="cancelPrompt"
1034
1046
  />
1035
1047
  </template>
@@ -381,6 +381,17 @@ export default {
381
381
  required: false,
382
382
  default: true,
383
383
  },
384
+ /**
385
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
386
+ * that are allowed to render as clickable links in markdown content.
387
+ * Links to other domains will not be clickable.
388
+ */
389
+ trustedUrls: {
390
+ type: Array,
391
+ required: false,
392
+ default: () => [],
393
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
394
+ },
384
395
  },
385
396
  data() {
386
397
  return {
@@ -894,6 +905,7 @@ export default {
894
905
  :with-feedback="withFeedback"
895
906
  :is-tool-approval-processing="isToolApprovalProcessing"
896
907
  :working-directory="workingDirectory"
908
+ :trusted-urls="trustedUrls"
897
909
  @track-feedback="onTrackFeedback"
898
910
  @insert-code-snippet="onInsertCodeSnippet"
899
911
  @copy-code-snippet="onCopyCodeSnippet"
@@ -49,12 +49,15 @@ export default {
49
49
  default: true,
50
50
  },
51
51
  /**
52
- * Base URL of the GitLab instance.
52
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
53
+ * that are allowed to render as clickable links in markdown content.
54
+ * Links to other domains will not be clickable.
53
55
  */
54
56
  trustedUrls: {
55
57
  type: Array,
56
58
  required: false,
57
59
  default: () => [],
60
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
58
61
  },
59
62
  /**
60
63
  * Whether the chat should show the feedback link on the assistant messages.
@@ -82,12 +82,15 @@ export default {
82
82
  required: true,
83
83
  },
84
84
  /**
85
- * Base URL of the GitLab instance.
85
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
86
+ * that are allowed to render as clickable links in markdown content.
87
+ * Links to other domains will not be clickable.
86
88
  */
87
89
  trustedUrls: {
88
90
  type: Array,
89
91
  required: false,
90
92
  default: () => [],
93
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
91
94
  },
92
95
  /**
93
96
  * Whether the chat should show the feedback link on the assistant messages.
@@ -149,20 +152,26 @@ export default {
149
152
  return this.message.contentHtml;
150
153
  }
151
154
 
152
- return this.renderMarkdown(this.message.content, this.trustedUrls);
155
+ return this.renderMarkdown(this.message.content, { trustedUrls: this.trustedUrls });
153
156
  },
154
157
  messageContent() {
155
158
  if (this.isAssistantMessage && this.isChunk) {
156
- return this.renderMarkdown(concatUntilEmpty(this.messageChunks), this.trustedUrls);
159
+ return this.renderMarkdown(concatUntilEmpty(this.messageChunks), {
160
+ trustedUrls: this.trustedUrls,
161
+ });
157
162
  }
158
163
 
159
164
  return (
160
- this.renderMarkdown(this.defaultContent, this.trustedUrls) ||
161
- this.renderMarkdown(concatUntilEmpty(this.message.chunks), this.trustedUrls)
165
+ this.renderMarkdown(this.defaultContent, { trustedUrls: this.trustedUrls }) ||
166
+ this.renderMarkdown(concatUntilEmpty(this.message.chunks), {
167
+ trustedUrls: this.trustedUrls,
168
+ })
162
169
  );
163
170
  },
164
171
  renderedError() {
165
- return this.renderMarkdown(this.message.errors?.join('; ') || '', this.trustedUrls);
172
+ return this.renderMarkdown(this.message.errors?.join('; ') || '', {
173
+ trustedUrls: this.trustedUrls,
174
+ });
166
175
  },
167
176
  error() {
168
177
  return Boolean(this.message?.errors?.length) && this.message.errors.join('; ');
@@ -189,13 +189,14 @@ export default {
189
189
  return this.approvalStatus === TOOL_STATUS.Approved;
190
190
  },
191
191
  isApproving() {
192
- return this.isProcessing && this.localProcessingState === PROCESSING_STATE.APPROVING;
192
+ return this.localProcessingState === PROCESSING_STATE.APPROVING || this.isProcessing;
193
193
  },
194
194
  isDenying() {
195
- return this.isProcessing && this.localProcessingState === PROCESSING_STATE.DENYING;
195
+ return this.localProcessingState === PROCESSING_STATE.DENYING;
196
196
  },
197
197
  buttonsDisabled() {
198
- return this.isProcessing;
198
+ // Disable if we've taken action locally OR if parent says we're processing
199
+ return this.localProcessingState !== PROCESSING_STATE.NONE || this.isProcessing;
199
200
  },
200
201
  approveButtonText() {
201
202
  return this.isApproving ? this.$options.i18n.APPROVING_TEXT : this.$options.i18n.APPROVE_TEXT;
@@ -221,52 +222,46 @@ export default {
221
222
  },
222
223
  },
223
224
  watch: {
224
- // Reset local state when processing completes
225
- isProcessing(newVal, oldVal) {
226
- if (oldVal === true && newVal === false) {
227
- this.resetProcessingState();
225
+ approvalStatus(newVal) {
226
+ if (newVal === TOOL_STATUS.Approved) {
227
+ this.collapsed = true;
228
228
  }
229
229
  },
230
230
  },
231
231
  methods: {
232
- handleApprove() {
233
- if (this.isProcessing) return;
232
+ approveWithType(approvalType) {
233
+ if (this.localProcessingState !== PROCESSING_STATE.NONE) {
234
+ return;
235
+ }
234
236
 
235
237
  this.localProcessingState = PROCESSING_STATE.APPROVING;
236
- // Default to primary option type for backward compatibility
237
- const approvalPayload = {
238
- type: this.primaryApprovalOption.type || acceptedApproveToolPayloads.APPROVE_TOOL_ONCE,
239
- };
240
- this.$emit('approve-tool', approvalPayload);
238
+ this.$emit('approve-tool', { type: approvalType });
239
+ },
240
+ handleApprove() {
241
+ const approvalType =
242
+ this.primaryApprovalOption.type || acceptedApproveToolPayloads.APPROVE_TOOL_ONCE;
243
+ this.approveWithType(approvalType);
241
244
  },
242
245
  handlePrimaryApprove() {
243
- if (this.isProcessing) return;
244
-
245
- this.localProcessingState = PROCESSING_STATE.APPROVING;
246
- this.$emit('approve-tool', { type: this.primaryApprovalOption.type });
246
+ this.approveWithType(this.primaryApprovalOption.type);
247
247
  },
248
248
  handleDropdownSelection(option) {
249
- if (this.isProcessing) return;
250
-
251
- this.localProcessingState = PROCESSING_STATE.APPROVING;
252
- this.$emit('approve-tool', { type: option.type });
249
+ this.approveWithType(option.type);
253
250
  },
254
251
  handleDeny() {
255
- if (this.isProcessing) return;
256
-
257
252
  this.showDenialReason = true;
258
253
  },
259
254
  cancelDenial() {
260
255
  this.showDenialReason = false;
261
256
  this.denialReason = '';
262
- this.localProcessingState = PROCESSING_STATE.NONE;
263
257
  },
264
258
  submitDenial() {
265
- if (this.isProcessing) return;
259
+ if (this.localProcessingState !== PROCESSING_STATE.NONE) {
260
+ return;
261
+ }
266
262
 
267
263
  this.localProcessingState = PROCESSING_STATE.DENYING;
268
264
  this.$emit('deny-tool', this.denialReason || null);
269
- // Don't reset state here - wait for isProcessing to change
270
265
  },
271
266
  resetProcessingState() {
272
267
  this.localProcessingState = PROCESSING_STATE.NONE;
@@ -297,12 +297,15 @@ export default {
297
297
  default: false,
298
298
  },
299
299
  /**
300
- * Base URL of the GitLab instance.
300
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
301
+ * that are allowed to render as clickable links in markdown content.
302
+ * Links to other domains will not be clickable.
301
303
  */
302
304
  trustedUrls: {
303
305
  type: Array,
304
306
  required: false,
305
307
  default: () => [],
308
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
306
309
  },
307
310
  /*
308
311
  * The preferred locale for the chat interface.
@@ -279,12 +279,15 @@ export default {
279
279
  default: false,
280
280
  },
281
281
  /**
282
- * Base URL of the GitLab instance.
282
+ * Array of trusted hostnames (e.g., ['gitlab.com', 'example.com'])
283
+ * that are allowed to render as clickable links in markdown content.
284
+ * Links to other domains will not be clickable.
283
285
  */
284
286
  trustedUrls: {
285
287
  type: Array,
286
288
  required: false,
287
289
  default: () => [],
290
+ validator: (urls) => urls.every((url) => typeof url === 'string'),
288
291
  },
289
292
  /*
290
293
  * The preferred locale for the chat interface.