@gitlab/duo-ui 13.10.1 → 13.10.2

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,11 @@
1
+ ## [13.10.2](https://gitlab.com/gitlab-org/duo-ui/compare/v13.10.1...v13.10.2) (2025-11-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **DuoChat:** Accept both Object and Array for messages prop ([ee52b96](https://gitlab.com/gitlab-org/duo-ui/commit/ee52b96ffc2bf03488e3037165124993ef59ac8f))
7
+ * **DuoChat:** Display all tools in multi-tool approval requests ([49a4bfc](https://gitlab.com/gitlab-org/duo-ui/commit/49a4bfccecf83fe6cfb7b0757a3b00a38140201a))
8
+
1
9
  ## [13.10.1](https://gitlab.com/gitlab-org/duo-ui/compare/v13.10.0...v13.10.1) (2025-11-10)
2
10
 
3
11
 
@@ -80,17 +80,37 @@ var script = {
80
80
  }
81
81
  },
82
82
  computed: {
83
- isAwaitingToolApproval() {
84
- const lastMessage = this.messages[this.messages.length - 1];
85
- return Boolean(lastMessage && lastMessage.message_type === 'request' && lastMessage.tool_info);
83
+ /**
84
+ * Checks if the last message in the conversation is a tool approval request
85
+ * @returns {boolean} True if last message is awaiting tool approval
86
+ */
87
+ isLastMessageToolApproval() {
88
+ const lastMsg = this.messages[this.messages.length - 1];
89
+ return (lastMsg === null || lastMsg === void 0 ? void 0 : lastMsg.message_type) === 'request' && Boolean(lastMsg === null || lastMsg === void 0 ? void 0 : lastMsg.tool_info);
90
+ },
91
+ /**
92
+ * Collects all consecutive tool approval requests at the end of the messages array.
93
+ * @returns {Array} Array of all pending tool approval requests
94
+ */
95
+ pendingToolApprovals() {
96
+ // Early return if last message isn't a tool request
97
+ if (!this.isLastMessageToolApproval) {
98
+ return [];
99
+ }
100
+
101
+ // Find the last non-tool message
102
+ const lastNonToolIndex = this.messages.findLastIndex(msg => msg.message_type !== 'request' || !msg.tool_info);
103
+
104
+ // Slice from after the last non-tool message to the end
105
+ return this.messages.slice(lastNonToolIndex + 1);
86
106
  },
87
- lastMessage() {
88
- return this.messages[this.messages.length - 1];
107
+ hasPendingToolApprovals() {
108
+ return this.pendingToolApprovals.length > 0;
89
109
  },
90
110
  toolApprovalOptions() {
91
- var _this$lastMessage;
92
- // Extract approval options from the last message's data
93
- return (_this$lastMessage = this.lastMessage) === null || _this$lastMessage === void 0 ? void 0 : _this$lastMessage.approvalOptions;
111
+ var _this$pendingToolAppr;
112
+ const lastIndex = this.pendingToolApprovals.length - 1;
113
+ return (_this$pendingToolAppr = this.pendingToolApprovals[lastIndex]) === null || _this$pendingToolAppr === void 0 ? void 0 : _this$pendingToolAppr.approvalOptions;
94
114
  }
95
115
  },
96
116
  methods: {
@@ -132,7 +152,7 @@ const __vue_script__ = script;
132
152
  /* template */
133
153
  var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{class:[
134
154
  'gl-flex gl-flex-col gl-justify-end gl-gap-6',
135
- { 'insert-code-hidden': !_vm.enableCodeInsertion } ]},[(_vm.showDelimiter)?_c('div',{staticClass:"gl-my-5 gl-flex gl-items-center gl-gap-4 gl-text-gray-500",attrs:{"data-testid":"conversation-delimiter"}},[_c('hr',{staticClass:"gl-grow"}),_vm._v(" "),_c('span',[_vm._v(_vm._s(_vm.$options.i18n.CONVERSATION_NEW_CHAT))]),_vm._v(" "),_c('hr',{staticClass:"gl-grow"})]):_vm._e(),_vm._v(" "),_vm._l((_vm.messages),function(msg,index){return _c('duo-chat-message',{key:((msg.role) + "-" + index),attrs:{"message":msg,"trusted-urls":_vm.trustedUrls,"is-cancelled":_vm.canceledRequestIds.includes(msg.requestId),"with-feedback":_vm.withFeedback,"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,"open-file-path":_vm.onOpenFilePath}})}),_vm._v(" "),(_vm.isAwaitingToolApproval)?_c('duo-chat-message-tool-approval',{attrs:{"message":_vm.lastMessage,"is-processing":_vm.isToolApprovalProcessing,"approval-options":_vm.toolApprovalOptions},on:{"approve-tool":_vm.onApproveToolCall,"deny-tool":_vm.onDenyToolCall}}):_vm._e()],2)};
155
+ { 'insert-code-hidden': !_vm.enableCodeInsertion } ]},[(_vm.showDelimiter)?_c('div',{staticClass:"gl-my-5 gl-flex gl-items-center gl-gap-4 gl-text-gray-500",attrs:{"data-testid":"conversation-delimiter"}},[_c('hr',{staticClass:"gl-grow"}),_vm._v(" "),_c('span',[_vm._v(_vm._s(_vm.$options.i18n.CONVERSATION_NEW_CHAT))]),_vm._v(" "),_c('hr',{staticClass:"gl-grow"})]):_vm._e(),_vm._v(" "),_vm._l((_vm.messages),function(msg,index){return _c('duo-chat-message',{key:((msg.role) + "-" + index),attrs:{"message":msg,"trusted-urls":_vm.trustedUrls,"is-cancelled":_vm.canceledRequestIds.includes(msg.requestId),"with-feedback":_vm.withFeedback,"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,"open-file-path":_vm.onOpenFilePath}})}),_vm._v(" "),(_vm.hasPendingToolApprovals)?_c('duo-chat-message-tool-approval',{attrs:{"messages":_vm.pendingToolApprovals,"is-processing":_vm.isToolApprovalProcessing,"approval-options":_vm.toolApprovalOptions},on:{"approve-tool":_vm.onApproveToolCall,"deny-tool":_vm.onDenyToolCall}}):_vm._e()],2)};
136
156
  var __vue_staticRenderFns__ = [];
137
157
 
138
158
  /* style */
@@ -177,9 +177,9 @@ var script = {
177
177
  const __vue_script__ = script;
178
178
 
179
179
  /* template */
180
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('base-message',{attrs:{"message":_vm.message},scopedSlots:_vm._u([{key:"message",fn:function(ref){
180
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.requiresApproval)?_c('message-tool-approval',{attrs:{"messages":[_vm.message],"working-directory":_vm.workingDirectory,"approval-status":"approved"}}):_c('base-message',{attrs:{"message":_vm.message},scopedSlots:_vm._u([{key:"message",fn:function(ref){
181
181
  var content = ref.content;
182
- return [(_vm.requiresApproval)?_c('message-tool-approval',{attrs:{"message":_vm.message,"working-directory":_vm.workingDirectory,"approval-status":"approved"}}):_c('div',{staticClass:"gl-border gl-flex-col gl-rounded-lg gl-border-default gl-p-4"},[_c('div',{staticClass:"gl-flex gl-flex-col gl-gap-2 gl-text-base gl-text-subtle"},[_c('div',{staticClass:"gl-flex gl-items-start gl-gap-x-3"},[_c('div',{staticClass:"gl-mt-1 gl-flex gl-flex-shrink-0"},[_c('gl-icon',{attrs:{"name":_vm.iconName}})],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-min-w-0 gl-grow gl-flex-wrap gl-items-center gl-gap-x-2"},[_c('span',{attrs:{"data-testid":"tool-message-content"}},[_vm._v(_vm._s(content))]),_vm._v(" "),(_vm.shouldShowFilePath)?_c('gl-link',{staticClass:"file-path-link gl-min-w-0 gl-break-all gl-font-monospace",attrs:{"data-testid":"tool-message-file-path-link"},on:{"click":function($event){$event.preventDefault();return _vm.onOpenFilePath(_vm.messageFilePath)}}},[_c('code',{staticClass:"gl-rounded-base gl-px-2 gl-py-1 gl-text-default"},[_vm._v(_vm._s(_vm.messageFilePath))])]):_vm._e()],1),_vm._v(" "),(_vm.shouldShowDetails)?_c('gl-button',{staticClass:"-gl-mt-1",attrs:{"category":"tertiary","size":"small","icon":_vm.collapseIconName,"aria-label":_vm.isDetailsOpen ? _vm.$options.i18n.COLLAPSE_DETAILS : _vm.$options.i18n.EXPAND_DETAILS,"data-testid":"tool-message-toggle-button"},on:{"click":_vm.toggleDetails}}):_vm._e()],1),_vm._v(" "),(_vm.hasMetadataToShow)?_c('div',{staticClass:"gl-ml-5 gl-flex gl-flex-wrap gl-gap-2 gl-text-sm"},[(_vm.shouldShowProjectId)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-project-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.PROJECT_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-project-id"}},[_vm._v(_vm._s(_vm.projectId))])]):_vm._e(),_vm._v(" "),(_vm.shouldShowBranch)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-branch-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.BRANCH_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-branch-name"}},[_vm._v(_vm._s(_vm.branchName))])]):_vm._e(),_vm._v(" "),(_vm.shouldShowStartBranch)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-start-branch-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.START_BRANCH_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-start-branch-name"}},[_vm._v(_vm._s(_vm.startBranch))])]):_vm._e()]):_vm._e()]),_vm._v(" "),(_vm.shouldShowDetails)?_c('gl-collapse',{staticClass:"gl-overflow-hidden",attrs:{"visible":_vm.isDetailsOpen}},[_c('message-tool-details',{attrs:{"message":_vm.message,"is-expanded":_vm.isDetailsOpen},on:{"copy-code-snippet":_vm.onCopyCodeSnippet}})],1):_vm._e()],1)]}}])})};
182
+ return [_c('div',{staticClass:"gl-border gl-flex-col gl-rounded-lg gl-border-default gl-p-4"},[_c('div',{staticClass:"gl-flex gl-flex-col gl-gap-2 gl-text-base gl-text-subtle"},[_c('div',{staticClass:"gl-flex gl-items-start gl-gap-x-3"},[_c('div',{staticClass:"gl-mt-1 gl-flex gl-flex-shrink-0"},[_c('gl-icon',{attrs:{"name":_vm.iconName}})],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-min-w-0 gl-grow gl-flex-wrap gl-items-center gl-gap-x-2"},[_c('span',{attrs:{"data-testid":"tool-message-content"}},[_vm._v(_vm._s(content))]),_vm._v(" "),(_vm.shouldShowFilePath)?_c('gl-link',{staticClass:"file-path-link gl-min-w-0 gl-break-all gl-font-monospace",attrs:{"data-testid":"tool-message-file-path-link"},on:{"click":function($event){$event.preventDefault();return _vm.onOpenFilePath(_vm.messageFilePath)}}},[_c('code',{staticClass:"gl-rounded-base gl-px-2 gl-py-1 gl-text-default"},[_vm._v(_vm._s(_vm.messageFilePath))])]):_vm._e()],1),_vm._v(" "),(_vm.shouldShowDetails)?_c('gl-button',{staticClass:"-gl-mt-1",attrs:{"category":"tertiary","size":"small","icon":_vm.collapseIconName,"aria-label":_vm.isDetailsOpen ? _vm.$options.i18n.COLLAPSE_DETAILS : _vm.$options.i18n.EXPAND_DETAILS,"data-testid":"tool-message-toggle-button"},on:{"click":_vm.toggleDetails}}):_vm._e()],1),_vm._v(" "),(_vm.hasMetadataToShow)?_c('div',{staticClass:"gl-ml-5 gl-flex gl-flex-wrap gl-gap-2 gl-text-sm"},[(_vm.shouldShowProjectId)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-project-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.PROJECT_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-project-id"}},[_vm._v(_vm._s(_vm.projectId))])]):_vm._e(),_vm._v(" "),(_vm.shouldShowBranch)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-branch-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.BRANCH_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-branch-name"}},[_vm._v(_vm._s(_vm.branchName))])]):_vm._e(),_vm._v(" "),(_vm.shouldShowStartBranch)?_c('div',{staticClass:"gl-flex gl-min-w-0 gl-gap-x-2 gl-rounded-full gl-bg-strong gl-px-3",attrs:{"data-testid":"tool-message-start-branch-info"}},[_c('span',{staticClass:"gl-whitespace-nowrap"},[_vm._v(_vm._s(_vm.$options.i18n.START_BRANCH_LABEL)+":")]),_vm._v(" "),_c('span',{staticClass:"gl-truncate",attrs:{"data-testid":"tool-message-start-branch-name"}},[_vm._v(_vm._s(_vm.startBranch))])]):_vm._e()]):_vm._e()]),_vm._v(" "),(_vm.shouldShowDetails)?_c('gl-collapse',{staticClass:"gl-overflow-hidden",attrs:{"visible":_vm.isDetailsOpen}},[_c('message-tool-details',{attrs:{"message":_vm.message,"is-expanded":_vm.isDetailsOpen},on:{"copy-code-snippet":_vm.onCopyCodeSnippet}})],1):_vm._e()],1)]}}])})};
183
183
  var __vue_staticRenderFns__ = [];
184
184
 
185
185
  /* style */
@@ -50,7 +50,8 @@ const i18n = {
50
50
  [TOOL_STATUS.Pending]: translate('MessageToolApproval.toolStatusPending', 'Pending'),
51
51
  [TOOL_STATUS.Approved]: translate('MessageToolApproval.toolStatusApproved', 'Approved'),
52
52
  TOGGLE_PARAMS_BUTTON_EXPAND: translate('MessageToolApproval.collapseButtonCollapsed', 'Display tool details'),
53
- TOGGLE_PARAMS_BUTTON_COLLAPSE: translate('MessageToolApproval.collapseButtonExpanded', 'Hide tool details')
53
+ TOGGLE_PARAMS_BUTTON_COLLAPSE: translate('MessageToolApproval.collapseButtonExpanded', 'Hide tool details'),
54
+ MULTI_TOOL_TITLE: translate('MessageToolApproval.multiToolTitle', 'Duo wants to execute %{count} tools.')
54
55
  };
55
56
  const TOOL_PARAMS_VIEW_COMPONENTS = {
56
57
  [APPROVAL_TOOL_NAMES.createCommit]: 'CreateCommitToolParams',
@@ -84,9 +85,9 @@ var script = {
84
85
  PreBlock
85
86
  },
86
87
  props: {
87
- message: {
88
+ messages: {
88
89
  required: true,
89
- type: Object
90
+ type: Array
90
91
  },
91
92
  workingDirectory: {
92
93
  type: String,
@@ -134,17 +135,23 @@ var script = {
134
135
  };
135
136
  },
136
137
  computed: {
138
+ multipleApprovalsNeeded() {
139
+ return this.messages.length > 1;
140
+ },
141
+ primaryMessage() {
142
+ return this.messages[0] || null;
143
+ },
137
144
  toolName() {
138
- var _this$message, _this$message$tool_in;
139
- return ((_this$message = this.message) === null || _this$message === void 0 ? void 0 : (_this$message$tool_in = _this$message.tool_info) === null || _this$message$tool_in === void 0 ? void 0 : _this$message$tool_in.name) || this.$options.i18n.TOOL_UNKNOWN;
145
+ var _this$primaryMessage, _this$primaryMessage$;
146
+ return ((_this$primaryMessage = this.primaryMessage) === null || _this$primaryMessage === void 0 ? void 0 : (_this$primaryMessage$ = _this$primaryMessage.tool_info) === null || _this$primaryMessage$ === void 0 ? void 0 : _this$primaryMessage$.name) || this.$options.i18n.TOOL_UNKNOWN;
140
147
  },
141
148
  toolParameters() {
142
- var _this$message2, _this$message2$tool_i;
143
- return ((_this$message2 = this.message) === null || _this$message2 === void 0 ? void 0 : (_this$message2$tool_i = _this$message2.tool_info) === null || _this$message2$tool_i === void 0 ? void 0 : _this$message2$tool_i.args) || {};
149
+ var _this$primaryMessage2, _this$primaryMessage3;
150
+ return ((_this$primaryMessage2 = this.primaryMessage) === null || _this$primaryMessage2 === void 0 ? void 0 : (_this$primaryMessage3 = _this$primaryMessage2.tool_info) === null || _this$primaryMessage3 === void 0 ? void 0 : _this$primaryMessage3.args) || {};
144
151
  },
145
152
  toolResponse() {
146
- var _this$message3, _this$message3$tool_i;
147
- return (_this$message3 = this.message) === null || _this$message3 === void 0 ? void 0 : (_this$message3$tool_i = _this$message3.tool_info) === null || _this$message3$tool_i === void 0 ? void 0 : _this$message3$tool_i.tool_response;
153
+ var _this$primaryMessage4, _this$primaryMessage5;
154
+ return (_this$primaryMessage4 = this.primaryMessage) === null || _this$primaryMessage4 === void 0 ? void 0 : (_this$primaryMessage5 = _this$primaryMessage4.tool_info) === null || _this$primaryMessage5 === void 0 ? void 0 : _this$primaryMessage5.tool_response;
148
155
  },
149
156
  camelCaseToolParameters() {
150
157
  return convertKeysToCamelCase(this.toolParameters);
@@ -153,6 +160,12 @@ var script = {
153
160
  return Object.keys(this.toolParameters).length > 0;
154
161
  },
155
162
  toolApprovalTitle() {
163
+ // Multiple tools: show generic count message
164
+ if (this.multipleApprovalsNeeded) {
165
+ return this.$options.i18n.MULTI_TOOL_TITLE.replace('%{count}', this.messages.length);
166
+ }
167
+
168
+ // Single tool: show specific tool message
156
169
  return i18n.TOOL_APPROVAL_TITLES[this.toolName] || this.toolName;
157
170
  },
158
171
  toolStatusLabel() {
@@ -262,6 +275,12 @@ var script = {
262
275
  this.localProcessingState = PROCESSING_STATE.NONE;
263
276
  this.showDenialReason = false;
264
277
  this.denialReason = '';
278
+ },
279
+ convertKeysToCamelCase(obj) {
280
+ return convertKeysToCamelCase(obj);
281
+ },
282
+ getToolParamsComponent(toolName) {
283
+ return TOOL_PARAMS_VIEW_COMPONENTS[toolName];
265
284
  }
266
285
  },
267
286
  i18n,
@@ -272,7 +291,11 @@ var script = {
272
291
  const __vue_script__ = script;
273
292
 
274
293
  /* template */
275
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-card',{staticClass:"gl-w-full",class:{ 'message-tool-approval-collapsed': _vm.collapsed },attrs:{"header-class":{ 'message-tool-approval-collapsed-header': _vm.collapsed },"body-class":{ 'gl-hidden': _vm.collapsed }},scopedSlots:_vm._u([{key:"header",fn:function(){return [_c('div',{staticClass:"gl-flex gl-items-center gl-justify-between gl-gap-3"},[_c('span',[(_vm.collapsible)?_c('gl-button',_vm._b({attrs:{"variant":"default","category":"tertiary","size":"small","data-testid":"toggle-details-button"},on:{"click":function($event){_vm.collapsed = !_vm.collapsed;}}},'gl-button',_vm.collapseButtonProps,false)):_vm._e(),_vm._v(" "),_c('span',[_vm._v("\n "+_vm._s(_vm.toolApprovalTitle)+"\n ")])],1),_vm._v(" "),_c('gl-badge',{attrs:{"variant":_vm.toolStatusVariant}},[_vm._v("\n "+_vm._s(_vm.toolStatusLabel)+"\n ")])],1)]},proxy:true},(!_vm.isApproved)?{key:"footer",fn:function(){return [(!_vm.showDenialReason)?_c('div',{staticClass:"gl-flex gl-gap-2"},[(_vm.showSplitButton)?_c('gl-dropdown',{attrs:{"variant":"confirm","size":"small","split":"","text":_vm.primaryApprovalOption.text,"disabled":_vm.buttonsDisabled || _vm.primaryApprovalOption.disabled,"loading":_vm.isApproving,"data-testid":"approve-dropdown"},on:{"click":_vm.handlePrimaryApprove}},_vm._l((_vm.additionalApprovalOptions),function(option){return _c('gl-dropdown-item',{key:option.type,attrs:{"disabled":option.disabled,"data-testid":"approve-dropdown-item"},on:{"click":function($event){return _vm.handleDropdownSelection(option)}}},[_vm._v("\n "+_vm._s(option.text)+"\n ")])}),1):_c('gl-button',{attrs:{"variant":"confirm","size":"small","data-testid":"approve-tool-inline","disabled":_vm.buttonsDisabled || _vm.primaryApprovalOption.disabled,"loading":_vm.isApproving},on:{"click":_vm.handlePrimaryApprove}},[_vm._v("\n "+_vm._s(_vm.primaryApprovalOption.text)+"\n ")]),_vm._v(" "),_c('gl-button',{attrs:{"size":"small","data-testid":"deny-tool-inline","disabled":_vm.buttonsDisabled,"loading":_vm.isDenying},on:{"click":_vm.handleDeny}},[_vm._v("\n "+_vm._s(_vm.denyButtonText)+"\n ")])],1):_c('div',[_c('gl-form-group',{staticClass:"gl-mb-3",attrs:{"label":_vm.$options.i18n.DENIAL_REASON_LABEL,"label-for":"inline-rejection-reason","optional":true}},[_c('gl-form-textarea',{attrs:{"id":"inline-rejection-reason","placeholder":_vm.$options.i18n.DENIAL_REASON_PLACEHOLDER,"rows":2,"no-resize":true,"submit-on-enter":false,"disabled":_vm.buttonsDisabled,"data-testid":"denial-reason-textarea","autofocus":""},on:{"submit":_vm.submitDenial},model:{value:(_vm.denialReason),callback:function ($$v) {_vm.denialReason=$$v;},expression:"denialReason"}})],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-gap-3"},[_c('gl-button',{attrs:{"size":"small","data-testid":"submit-denial","variant":"confirm","disabled":_vm.buttonsDisabled,"loading":_vm.isDenying},on:{"click":_vm.submitDenial}},[_vm._v("\n "+_vm._s(_vm.denyButtonText)+"\n ")]),_vm._v(" "),_c('gl-button',{attrs:{"size":"small","data-testid":"cancel-denial","disabled":_vm.buttonsDisabled},on:{"click":_vm.cancelDenial}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CANCEL_TEXT)+"\n ")])],1)],1)]},proxy:true}:null],null,true)},[_vm._v(" "),(_vm.toolParamsViewComponent)?_c(_vm.toolParamsViewComponent,_vm._b({tag:"component",staticClass:"gl-leading-20",attrs:{"tool-name":_vm.toolName,"tool-params":_vm.camelCaseToolParameters,"tool-response":_vm.toolResponse,"working-directory":_vm.workingDirectory}},'component',_vm.camelCaseToolParameters,false)):_c('figure',{staticClass:"gl-m-0 gl-flex gl-flex-col gl-gap-2"},[_c('figcaption',{staticClass:"gl-text-subtle"},[_vm._v("\n "+_vm._s(_vm.$options.i18n.REQUEST_TEXT)+"\n ")]),_vm._v(" "),(_vm.hasToolParameters)?_c('pre-block',{attrs:{"data-testid":"tool-parameters"}},[_vm._v(_vm._s(JSON.stringify(_vm.toolParameters, null, 2)))]):_c('span',{staticClass:"gl-text-sm gl-text-gray-500",attrs:{"data-testid":"no-parameters-message"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.NO_PARAMETERS_TEXT)+"\n ")])],1)],1)};
294
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-card',{staticClass:"gl-w-full",class:{ 'message-tool-approval-collapsed': _vm.collapsed },attrs:{"header-class":{ 'message-tool-approval-collapsed-header': _vm.collapsed },"body-class":{ 'gl-hidden': _vm.collapsed }},scopedSlots:_vm._u([{key:"header",fn:function(){return [_c('div',{staticClass:"gl-flex gl-items-center gl-justify-between gl-gap-3"},[_c('span',[(_vm.collapsible)?_c('gl-button',_vm._b({attrs:{"variant":"default","category":"tertiary","size":"small","data-testid":"toggle-details-button"},on:{"click":function($event){_vm.collapsed = !_vm.collapsed;}}},'gl-button',_vm.collapseButtonProps,false)):_vm._e(),_vm._v(" "),_c('span',[_vm._v("\n "+_vm._s(_vm.toolApprovalTitle)+"\n ")])],1),_vm._v(" "),_c('gl-badge',{attrs:{"variant":_vm.toolStatusVariant}},[_vm._v("\n "+_vm._s(_vm.toolStatusLabel)+"\n ")])],1)]},proxy:true},(!_vm.isApproved)?{key:"footer",fn:function(){return [(!_vm.showDenialReason)?_c('div',{staticClass:"gl-flex gl-gap-2"},[(_vm.showSplitButton)?_c('gl-dropdown',{attrs:{"variant":"confirm","size":"small","split":"","text":_vm.primaryApprovalOption.text,"disabled":_vm.buttonsDisabled || _vm.primaryApprovalOption.disabled,"loading":_vm.isApproving,"data-testid":"approve-dropdown"},on:{"click":_vm.handlePrimaryApprove}},_vm._l((_vm.additionalApprovalOptions),function(option){return _c('gl-dropdown-item',{key:option.type,attrs:{"disabled":option.disabled,"data-testid":"approve-dropdown-item"},on:{"click":function($event){return _vm.handleDropdownSelection(option)}}},[_vm._v("\n "+_vm._s(option.text)+"\n ")])}),1):_c('gl-button',{attrs:{"variant":"confirm","size":"small","data-testid":"approve-tool-inline","disabled":_vm.buttonsDisabled || _vm.primaryApprovalOption.disabled,"loading":_vm.isApproving},on:{"click":_vm.handlePrimaryApprove}},[_vm._v("\n "+_vm._s(_vm.primaryApprovalOption.text)+"\n ")]),_vm._v(" "),_c('gl-button',{attrs:{"size":"small","data-testid":"deny-tool-inline","disabled":_vm.buttonsDisabled,"loading":_vm.isDenying},on:{"click":_vm.handleDeny}},[_vm._v("\n "+_vm._s(_vm.denyButtonText)+"\n ")])],1):_c('div',[_c('gl-form-group',{staticClass:"gl-mb-3",attrs:{"label":_vm.$options.i18n.DENIAL_REASON_LABEL,"label-for":"inline-rejection-reason","optional":true}},[_c('gl-form-textarea',{attrs:{"id":"inline-rejection-reason","placeholder":_vm.$options.i18n.DENIAL_REASON_PLACEHOLDER,"rows":2,"no-resize":true,"submit-on-enter":false,"disabled":_vm.buttonsDisabled,"data-testid":"denial-reason-textarea","autofocus":""},on:{"submit":_vm.submitDenial},model:{value:(_vm.denialReason),callback:function ($$v) {_vm.denialReason=$$v;},expression:"denialReason"}})],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-gap-3"},[_c('gl-button',{attrs:{"size":"small","data-testid":"submit-denial","variant":"confirm","disabled":_vm.buttonsDisabled,"loading":_vm.isDenying},on:{"click":_vm.submitDenial}},[_vm._v("\n "+_vm._s(_vm.denyButtonText)+"\n ")]),_vm._v(" "),_c('gl-button',{attrs:{"size":"small","data-testid":"cancel-denial","disabled":_vm.buttonsDisabled},on:{"click":_vm.cancelDenial}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CANCEL_TEXT)+"\n ")])],1)],1)]},proxy:true}:null],null,true)},[_vm._v(" "),_vm._l((_vm.messages),function(toolMsg,index){return _c('div',{key:toolMsg.id || index},[(_vm.getToolParamsComponent(toolMsg.tool_info && toolMsg.tool_info.name))?_c(_vm.getToolParamsComponent(toolMsg.tool_info && toolMsg.tool_info.name),_vm._b({tag:"component",class:['gl-leading-20', { 'gl-border-t gl-mt-3 gl-border-gray-100 gl-pt-3': index > 0 }],attrs:{"tool-name":toolMsg.tool_info && toolMsg.tool_info.name,"tool-params":_vm.convertKeysToCamelCase((toolMsg.tool_info && toolMsg.tool_info.args) || {}),"tool-response":toolMsg.tool_info && toolMsg.tool_info.tool_response,"working-directory":_vm.workingDirectory}},'component',_vm.convertKeysToCamelCase((toolMsg.tool_info && toolMsg.tool_info.args) || {}),false)):_c('figure',{staticClass:"gl-m-0 gl-flex gl-flex-col gl-gap-2",class:{ 'gl-border-t gl-mt-3 gl-border-gray-100 gl-pt-3': index > 0 }},[_c('figcaption',{staticClass:"gl-text-subtle"},[_vm._v("\n "+_vm._s(_vm.$options.i18n.REQUEST_TEXT)+"\n ")]),_vm._v(" "),(
295
+ toolMsg.tool_info &&
296
+ toolMsg.tool_info.args &&
297
+ Object.keys(toolMsg.tool_info.args).length > 0
298
+ )?_c('pre-block',{attrs:{"data-testid":"tool-parameters"}},[_vm._v(_vm._s(JSON.stringify(toolMsg.tool_info.args, null, 2)))]):_c('span',{staticClass:"gl-text-sm gl-text-gray-500",attrs:{"data-testid":"no-parameters-message"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.NO_PARAMETERS_TEXT)+"\n ")])],1)],1)})],2)};
276
299
  var __vue_staticRenderFns__ = [];
277
300
 
278
301
  /* style */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/duo-ui",
3
- "version": "13.10.1",
3
+ "version": "13.10.2",
4
4
  "description": "Duo UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -82,18 +82,38 @@ export default {
82
82
  },
83
83
  },
84
84
  computed: {
85
- isAwaitingToolApproval() {
86
- const lastMessage = this.messages[this.messages.length - 1];
87
- return Boolean(
88
- lastMessage && lastMessage.message_type === 'request' && lastMessage.tool_info
85
+ /**
86
+ * Checks if the last message in the conversation is a tool approval request
87
+ * @returns {boolean} True if last message is awaiting tool approval
88
+ */
89
+ isLastMessageToolApproval() {
90
+ const lastMsg = this.messages[this.messages.length - 1];
91
+ return lastMsg?.message_type === 'request' && Boolean(lastMsg?.tool_info);
92
+ },
93
+ /**
94
+ * Collects all consecutive tool approval requests at the end of the messages array.
95
+ * @returns {Array} Array of all pending tool approval requests
96
+ */
97
+ pendingToolApprovals() {
98
+ // Early return if last message isn't a tool request
99
+ if (!this.isLastMessageToolApproval) {
100
+ return [];
101
+ }
102
+
103
+ // Find the last non-tool message
104
+ const lastNonToolIndex = this.messages.findLastIndex(
105
+ (msg) => msg.message_type !== 'request' || !msg.tool_info
89
106
  );
107
+
108
+ // Slice from after the last non-tool message to the end
109
+ return this.messages.slice(lastNonToolIndex + 1);
90
110
  },
91
- lastMessage() {
92
- return this.messages[this.messages.length - 1];
111
+ hasPendingToolApprovals() {
112
+ return this.pendingToolApprovals.length > 0;
93
113
  },
94
114
  toolApprovalOptions() {
95
- // Extract approval options from the last message's data
96
- return this.lastMessage?.approvalOptions;
115
+ const lastIndex = this.pendingToolApprovals.length - 1;
116
+ return this.pendingToolApprovals[lastIndex]?.approvalOptions;
97
117
  },
98
118
  },
99
119
  methods: {
@@ -161,8 +181,8 @@ export default {
161
181
  @open-file-path="onOpenFilePath"
162
182
  />
163
183
  <duo-chat-message-tool-approval
164
- v-if="isAwaitingToolApproval"
165
- :message="lastMessage"
184
+ v-if="hasPendingToolApprovals"
185
+ :messages="pendingToolApprovals"
166
186
  :is-processing="isToolApprovalProcessing"
167
187
  :approval-options="toolApprovalOptions"
168
188
  @approve-tool="onApproveToolCall"
@@ -171,15 +171,15 @@ export default {
171
171
  };
172
172
  </script>
173
173
  <template>
174
- <base-message :message="message">
174
+ <message-tool-approval
175
+ v-if="requiresApproval"
176
+ :messages="[message]"
177
+ :working-directory="workingDirectory"
178
+ approval-status="approved"
179
+ />
180
+ <base-message v-else :message="message">
175
181
  <template #message="{ content }">
176
- <message-tool-approval
177
- v-if="requiresApproval"
178
- :message="message"
179
- :working-directory="workingDirectory"
180
- approval-status="approved"
181
- />
182
- <div v-else class="gl-border gl-flex-col gl-rounded-lg gl-border-default gl-p-4">
182
+ <div class="gl-border gl-flex-col gl-rounded-lg gl-border-default gl-p-4">
183
183
  <div class="gl-flex gl-flex-col gl-gap-2 gl-text-base gl-text-subtle">
184
184
  <div class="gl-flex gl-items-start gl-gap-x-3">
185
185
  <div class="gl-mt-1 gl-flex gl-flex-shrink-0">
@@ -115,6 +115,10 @@ export const i18n = {
115
115
  'MessageToolApproval.collapseButtonExpanded',
116
116
  'Hide tool details'
117
117
  ),
118
+ MULTI_TOOL_TITLE: translate(
119
+ 'MessageToolApproval.multiToolTitle',
120
+ 'Duo wants to execute %{count} tools.'
121
+ ),
118
122
  };
119
123
 
120
124
  const TOOL_PARAMS_VIEW_COMPONENTS = {
@@ -150,9 +154,9 @@ export default {
150
154
  PreBlock,
151
155
  },
152
156
  props: {
153
- message: {
157
+ messages: {
154
158
  required: true,
155
- type: Object,
159
+ type: Array,
156
160
  },
157
161
  workingDirectory: {
158
162
  type: String,
@@ -204,14 +208,20 @@ export default {
204
208
  };
205
209
  },
206
210
  computed: {
211
+ multipleApprovalsNeeded() {
212
+ return this.messages.length > 1;
213
+ },
214
+ primaryMessage() {
215
+ return this.messages[0] || null;
216
+ },
207
217
  toolName() {
208
- return this.message?.tool_info?.name || this.$options.i18n.TOOL_UNKNOWN;
218
+ return this.primaryMessage?.tool_info?.name || this.$options.i18n.TOOL_UNKNOWN;
209
219
  },
210
220
  toolParameters() {
211
- return this.message?.tool_info?.args || {};
221
+ return this.primaryMessage?.tool_info?.args || {};
212
222
  },
213
223
  toolResponse() {
214
- return this.message?.tool_info?.tool_response;
224
+ return this.primaryMessage?.tool_info?.tool_response;
215
225
  },
216
226
  camelCaseToolParameters() {
217
227
  return convertKeysToCamelCase(this.toolParameters);
@@ -220,6 +230,12 @@ export default {
220
230
  return Object.keys(this.toolParameters).length > 0;
221
231
  },
222
232
  toolApprovalTitle() {
233
+ // Multiple tools: show generic count message
234
+ if (this.multipleApprovalsNeeded) {
235
+ return this.$options.i18n.MULTI_TOOL_TITLE.replace('%{count}', this.messages.length);
236
+ }
237
+
238
+ // Single tool: show specific tool message
223
239
  return i18n.TOOL_APPROVAL_TITLES[this.toolName] || this.toolName;
224
240
  },
225
241
  toolStatusLabel() {
@@ -327,6 +343,12 @@ export default {
327
343
  this.showDenialReason = false;
328
344
  this.denialReason = '';
329
345
  },
346
+ convertKeysToCamelCase(obj) {
347
+ return convertKeysToCamelCase(obj);
348
+ },
349
+ getToolParamsComponent(toolName) {
350
+ return TOOL_PARAMS_VIEW_COMPONENTS[toolName];
351
+ },
330
352
  },
331
353
  i18n,
332
354
  APPROVAL_TOOL_NAMES,
@@ -361,27 +383,39 @@ export default {
361
383
  </gl-badge>
362
384
  </div>
363
385
  </template>
364
- <component
365
- :is="toolParamsViewComponent"
366
- v-if="toolParamsViewComponent"
367
- class="gl-leading-20"
368
- :tool-name="toolName"
369
- :tool-params="camelCaseToolParameters"
370
- :tool-response="toolResponse"
371
- :working-directory="workingDirectory"
372
- v-bind="camelCaseToolParameters"
373
- />
374
- <figure v-else class="gl-m-0 gl-flex gl-flex-col gl-gap-2">
375
- <figcaption class="gl-text-subtle">
376
- {{ $options.i18n.REQUEST_TEXT }}
377
- </figcaption>
378
- <pre-block v-if="hasToolParameters" data-testid="tool-parameters">{{
379
- JSON.stringify(toolParameters, null, 2)
380
- }}</pre-block>
381
- <span v-else class="gl-text-sm gl-text-gray-500" data-testid="no-parameters-message">
382
- {{ $options.i18n.NO_PARAMETERS_TEXT }}
383
- </span>
384
- </figure>
386
+ <div v-for="(toolMsg, index) in messages" :key="toolMsg.id || index">
387
+ <component
388
+ :is="getToolParamsComponent(toolMsg.tool_info && toolMsg.tool_info.name)"
389
+ v-if="getToolParamsComponent(toolMsg.tool_info && toolMsg.tool_info.name)"
390
+ :class="['gl-leading-20', { 'gl-border-t gl-mt-3 gl-border-gray-100 gl-pt-3': index > 0 }]"
391
+ :tool-name="toolMsg.tool_info && toolMsg.tool_info.name"
392
+ :tool-params="convertKeysToCamelCase((toolMsg.tool_info && toolMsg.tool_info.args) || {})"
393
+ v-bind="convertKeysToCamelCase((toolMsg.tool_info && toolMsg.tool_info.args) || {})"
394
+ :tool-response="toolMsg.tool_info && toolMsg.tool_info.tool_response"
395
+ :working-directory="workingDirectory"
396
+ />
397
+ <figure
398
+ v-else
399
+ class="gl-m-0 gl-flex gl-flex-col gl-gap-2"
400
+ :class="{ 'gl-border-t gl-mt-3 gl-border-gray-100 gl-pt-3': index > 0 }"
401
+ >
402
+ <figcaption class="gl-text-subtle">
403
+ {{ $options.i18n.REQUEST_TEXT }}
404
+ </figcaption>
405
+ <pre-block
406
+ v-if="
407
+ toolMsg.tool_info &&
408
+ toolMsg.tool_info.args &&
409
+ Object.keys(toolMsg.tool_info.args).length > 0
410
+ "
411
+ data-testid="tool-parameters"
412
+ >{{ JSON.stringify(toolMsg.tool_info.args, null, 2) }}</pre-block
413
+ >
414
+ <span v-else class="gl-text-sm gl-text-gray-500" data-testid="no-parameters-message">
415
+ {{ $options.i18n.NO_PARAMETERS_TEXT }}
416
+ </span>
417
+ </figure>
418
+ </div>
385
419
  <template v-if="!isApproved" #footer>
386
420
  <div v-if="!showDenialReason" class="gl-flex gl-gap-2">
387
421
  <!-- Split button when multiple approval options available -->
package/translations.js CHANGED
@@ -163,6 +163,7 @@ export default {
163
163
  "Tell Duo why you're rejecting this tool execution...",
164
164
  'MessageToolApproval.denyText': 'Deny',
165
165
  'MessageToolApproval.denyingText': 'Denying...',
166
+ 'MessageToolApproval.multiToolTitle': 'Duo wants to execute %{count} tools.',
166
167
  'MessageToolApproval.noParametersText': 'No parameters will be sent with this request.',
167
168
  'MessageToolApproval.parametersText': 'Request parameters',
168
169
  'MessageToolApproval.runCommand': 'Duo wants to run a command.',