@gitlab/duo-ui 8.21.0 → 8.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [8.22.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.21.0...v8.22.0) (2025-06-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * **agentic-chat:** add tool approval flow component ([60630a0](https://gitlab.com/gitlab-org/duo-ui/commit/60630a09dd2f41e0eb4c8d9c9d45ae130f9e996f))
7
+
1
8
  # [8.21.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.20.0...v8.21.0) (2025-06-09)
2
9
 
3
10
 
@@ -0,0 +1,133 @@
1
+ import { translate } from '../../../../utils/i18n';
2
+ import AgenticToolApprovalModal from './agentic_tool_approval_modal/agentic_tool_approval_modal';
3
+ import AgenticToolRejectionModal from './agentic_tool_rejection_modal/agentic_tool_rejection_modal';
4
+ import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
5
+
6
+ const i18n = {
7
+ DEFAULT_DESCRIPTION: translate('AgenticToolApprovalFlow.defaultDescription', 'Duo Workflow wants to execute a tool. Do you want to proceed?')
8
+ };
9
+ const MODAL_TYPES = {
10
+ APPROVAL: 'approval',
11
+ REJECTION: 'rejection'
12
+ };
13
+ var script = {
14
+ name: 'AgenticToolApprovalFlow',
15
+ components: {
16
+ AgenticToolApprovalModal,
17
+ AgenticToolRejectionModal
18
+ },
19
+ props: {
20
+ /**
21
+ * Controls the visibility of the approval flow
22
+ */
23
+ visible: {
24
+ type: Boolean,
25
+ required: true
26
+ },
27
+ /**
28
+ * Details about the tool to be executed
29
+ * @property {string} name - The name of the tool
30
+ * @property {Object} parameters - The parameters to be passed to the tool
31
+ */
32
+ toolDetails: {
33
+ type: Object,
34
+ required: false,
35
+ default: () => ({}),
36
+ validator: value => {
37
+ return typeof value === 'object' && value !== null;
38
+ }
39
+ },
40
+ /**
41
+ * Custom description text for the approval request
42
+ */
43
+ description: {
44
+ type: String,
45
+ required: false,
46
+ default: i18n.DEFAULT_DESCRIPTION
47
+ }
48
+ },
49
+ data() {
50
+ return {
51
+ currentModal: MODAL_TYPES.APPROVAL
52
+ };
53
+ },
54
+ computed: {
55
+ showApprovalModal() {
56
+ return this.visible && this.currentModal === MODAL_TYPES.APPROVAL;
57
+ },
58
+ showRejectionModal() {
59
+ return this.visible && this.currentModal === MODAL_TYPES.REJECTION;
60
+ }
61
+ },
62
+ watch: {
63
+ visible(newVal) {
64
+ if (!newVal) {
65
+ this.currentModal = MODAL_TYPES.APPROVAL;
66
+ }
67
+ }
68
+ },
69
+ methods: {
70
+ handleApprove() {
71
+ /**
72
+ * Emitted when the user approves the tool execution
73
+ */
74
+ this.$emit('approve');
75
+ },
76
+ handleInitialDeny() {
77
+ this.currentModal = MODAL_TYPES.REJECTION;
78
+ },
79
+ handleForceDeny() {
80
+ this.$emit('deny', null);
81
+ },
82
+ handleRejectionSubmit(message) {
83
+ /**
84
+ * Emitted when the user denies the tool execution with an optional reason
85
+ * @param {string|null} reason - The rejection reason provided by the user
86
+ */
87
+ this.$emit('deny', message);
88
+ },
89
+ handleBack() {
90
+ this.currentModal = MODAL_TYPES.APPROVAL;
91
+ }
92
+ },
93
+ i18n
94
+ };
95
+
96
+ /* script */
97
+ const __vue_script__ = script;
98
+
99
+ /* template */
100
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{"data-testid":"agentic-tool-approval-flow"}},[_c('agentic-tool-approval-modal',{attrs:{"visible":_vm.showApprovalModal,"tool-details":_vm.toolDetails,"description":_vm.description},on:{"approve":_vm.handleApprove,"deny":_vm.handleInitialDeny,"deny-force":_vm.handleForceDeny}}),_vm._v(" "),_c('agentic-tool-rejection-modal',{attrs:{"visible":_vm.showRejectionModal},on:{"submit":_vm.handleRejectionSubmit,"back":_vm.handleBack}})],1)};
101
+ var __vue_staticRenderFns__ = [];
102
+
103
+ /* style */
104
+ const __vue_inject_styles__ = undefined;
105
+ /* scoped */
106
+ const __vue_scope_id__ = undefined;
107
+ /* module identifier */
108
+ const __vue_module_identifier__ = undefined;
109
+ /* functional template */
110
+ const __vue_is_functional_template__ = false;
111
+ /* style inject */
112
+
113
+ /* style inject SSR */
114
+
115
+ /* style inject shadow dom */
116
+
117
+
118
+
119
+ const __vue_component__ = __vue_normalize__(
120
+ { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
121
+ __vue_inject_styles__,
122
+ __vue_script__,
123
+ __vue_scope_id__,
124
+ __vue_is_functional_template__,
125
+ __vue_module_identifier__,
126
+ false,
127
+ undefined,
128
+ undefined,
129
+ undefined
130
+ );
131
+
132
+ export default __vue_component__;
133
+ export { i18n };
@@ -0,0 +1,137 @@
1
+ import { GlModal } from '@gitlab/ui';
2
+ import { translate } from '../../../../../utils/i18n';
3
+ import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
4
+
5
+ const i18n = {
6
+ TITLE: translate('AgenticToolApprovalModal.title', 'Tool Approval Required'),
7
+ DEFAULT_DESCRIPTION: translate('AgenticToolApprovalModal.defaultDescription', 'Duo Workflow wants to execute a tool. Do you want to proceed?'),
8
+ TOOL_LABEL: translate('AgenticToolApprovalModal.toolLabel', 'Tool:'),
9
+ TOOL_UNKNOWN: translate('AgenticToolApprovalModal.toolUnknown', 'Unknown'),
10
+ APPROVE_TEXT: translate('AgenticToolApprovalModal.approveText', 'Approve'),
11
+ DENY_TEXT: translate('AgenticToolApprovalModal.denyText', 'Deny')
12
+ };
13
+ var script = {
14
+ name: 'AgenticToolApprovalModal',
15
+ components: {
16
+ GlModal
17
+ },
18
+ props: {
19
+ /**
20
+ * Controls the visibility of the modal
21
+ */
22
+ visible: {
23
+ type: Boolean,
24
+ required: true
25
+ },
26
+ /**
27
+ * Details about the tool to be executed
28
+ * @property {string} name - The name of the tool
29
+ * @property {Object} parameters - The parameters to be passed to the tool
30
+ */
31
+ toolDetails: {
32
+ type: Object,
33
+ required: false,
34
+ default: () => ({}),
35
+ validator: value => {
36
+ return typeof value === 'object' && value !== null;
37
+ }
38
+ },
39
+ /**
40
+ * Custom description text for the approval request
41
+ */
42
+ description: {
43
+ type: String,
44
+ required: false,
45
+ default: i18n.DEFAULT_DESCRIPTION
46
+ }
47
+ },
48
+ computed: {
49
+ approveAction() {
50
+ return {
51
+ text: this.$options.i18n.APPROVE_TEXT,
52
+ attributes: {
53
+ variant: 'confirm',
54
+ icon: 'play',
55
+ size: 'small',
56
+ 'data-testid': 'approve-tool'
57
+ }
58
+ };
59
+ },
60
+ denyAction() {
61
+ return {
62
+ text: this.$options.i18n.DENY_TEXT,
63
+ attributes: {
64
+ variant: 'danger',
65
+ icon: 'cancel',
66
+ size: 'small',
67
+ 'data-testid': 'deny-tool'
68
+ }
69
+ };
70
+ },
71
+ toolName() {
72
+ var _this$toolDetails;
73
+ return ((_this$toolDetails = this.toolDetails) === null || _this$toolDetails === void 0 ? void 0 : _this$toolDetails.name) || this.$options.i18n.TOOL_UNKNOWN;
74
+ }
75
+ },
76
+ methods: {
77
+ handleApprove() {
78
+ /**
79
+ * Emitted when the user approves the tool execution
80
+ */
81
+ this.$emit('approve');
82
+ },
83
+ handleDeny() {
84
+ /**
85
+ * Emitted when the user denies the tool execution
86
+ */
87
+ this.$emit('deny');
88
+ },
89
+ handleModalHide(event) {
90
+ // If the modal is being hidden by the X button or ESC key,
91
+ // treat it as a deny-force action
92
+ if (event.trigger !== 'ok' && event.trigger !== 'cancel') {
93
+ this.$emit('deny-force');
94
+ }
95
+ }
96
+ },
97
+ i18n
98
+ };
99
+
100
+ /* script */
101
+ const __vue_script__ = script;
102
+
103
+ /* template */
104
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-modal',{attrs:{"visible":_vm.visible,"modal-id":"agentic-tool-approval-modal","title":_vm.$options.i18n.TITLE,"action-primary":_vm.approveAction,"action-cancel":_vm.denyAction,"no-close-on-backdrop":true,"no-close-on-esc":true,"data-testid":"agentic-tool-approval-modal"},on:{"primary":_vm.handleApprove,"canceled":_vm.handleDeny,"hide":_vm.handleModalHide}},[_c('div',{staticClass:"gl-mb-4"},[_c('p',{staticClass:"gl-mb-3",attrs:{"data-testid":"approval-description"}},[_vm._v("\n "+_vm._s(_vm.description)+"\n ")]),_vm._v(" "),(_vm.toolDetails)?_c('div',{staticClass:"gl-bg-gray-10 gl-p-3 gl-rounded-base gl-mb-3",attrs:{"data-testid":"tool-details"}},[_c('strong',[_vm._v(_vm._s(_vm.$options.i18n.TOOL_LABEL)+" "+_vm._s(_vm.toolName))]),_vm._v(" "),(_vm.toolDetails.parameters)?_c('pre',{staticClass:"gl-mt-2 gl-mb-0 gl-text-inherit",attrs:{"data-testid":"tool-parameters"}},[_vm._v(_vm._s(JSON.stringify(_vm.toolDetails.parameters, null, 2)))]):_vm._e()]):_vm._e()])])};
105
+ var __vue_staticRenderFns__ = [];
106
+
107
+ /* style */
108
+ const __vue_inject_styles__ = undefined;
109
+ /* scoped */
110
+ const __vue_scope_id__ = undefined;
111
+ /* module identifier */
112
+ const __vue_module_identifier__ = undefined;
113
+ /* functional template */
114
+ const __vue_is_functional_template__ = false;
115
+ /* style inject */
116
+
117
+ /* style inject SSR */
118
+
119
+ /* style inject shadow dom */
120
+
121
+
122
+
123
+ const __vue_component__ = __vue_normalize__(
124
+ { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
125
+ __vue_inject_styles__,
126
+ __vue_script__,
127
+ __vue_scope_id__,
128
+ __vue_is_functional_template__,
129
+ __vue_module_identifier__,
130
+ false,
131
+ undefined,
132
+ undefined,
133
+ undefined
134
+ );
135
+
136
+ export default __vue_component__;
137
+ export { i18n };
@@ -0,0 +1,128 @@
1
+ import { GlModal, GlFormTextarea, GlFormGroup } from '@gitlab/ui';
2
+ import { translate } from '../../../../../utils/i18n';
3
+ import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
4
+
5
+ const i18n = {
6
+ TITLE: translate('AgenticToolRejectionModal.title', 'Provide Rejection Reason'),
7
+ DESCRIPTION: translate('AgenticToolRejectionModal.description', 'You are about to reject the tool execution. Would you like to provide a reason?'),
8
+ LABEL: translate('AgenticToolRejectionModal.label', 'Rejection reason'),
9
+ PLACEHOLDER: translate('AgenticToolRejectionModal.placeholder', "Explain why you're rejecting this tool execution..."),
10
+ SUBMIT_TEXT: translate('AgenticToolRejectionModal.submitText', 'Submit Rejection'),
11
+ BACK_TEXT: translate('AgenticToolRejectionModal.backText', 'Back')
12
+ };
13
+ var script = {
14
+ name: 'AgenticToolRejectionModal',
15
+ components: {
16
+ GlModal,
17
+ GlFormTextarea,
18
+ GlFormGroup
19
+ },
20
+ props: {
21
+ /**
22
+ * Controls the visibility of the modal
23
+ */
24
+ visible: {
25
+ type: Boolean,
26
+ required: true
27
+ }
28
+ },
29
+ data() {
30
+ return {
31
+ rejectionMessage: ''
32
+ };
33
+ },
34
+ computed: {
35
+ submitAction() {
36
+ return {
37
+ text: this.$options.i18n.SUBMIT_TEXT,
38
+ attributes: {
39
+ variant: 'danger',
40
+ icon: 'cancel',
41
+ size: 'small',
42
+ 'data-testid': 'submit-rejection'
43
+ }
44
+ };
45
+ },
46
+ backAction() {
47
+ return {
48
+ text: this.$options.i18n.BACK_TEXT,
49
+ attributes: {
50
+ variant: 'default',
51
+ icon: 'chevron-left',
52
+ size: 'small',
53
+ 'data-testid': 'back-button'
54
+ }
55
+ };
56
+ }
57
+ },
58
+ watch: {
59
+ visible(newVal) {
60
+ if (!newVal) {
61
+ this.rejectionMessage = '';
62
+ }
63
+ }
64
+ },
65
+ methods: {
66
+ handleSubmit() {
67
+ /**
68
+ * Emitted when the user submits the rejection reason
69
+ * @param {string|null} reason - The rejection reason provided by the user, or null if empty
70
+ */
71
+ this.$emit('submit', this.rejectionMessage || null);
72
+ this.rejectionMessage = '';
73
+ },
74
+ handleBack() {
75
+ /**
76
+ * Emitted when the user clicks the back button to return to the approval modal
77
+ */
78
+ this.$emit('back');
79
+ },
80
+ handleModalHide(event) {
81
+ // If the modal is being hidden by the X button or ESC key,
82
+ // treat it as a deny-force action
83
+ if (event.trigger !== 'ok' && event.trigger !== 'cancel') {
84
+ this.handleBack();
85
+ }
86
+ }
87
+ },
88
+ i18n
89
+ };
90
+
91
+ /* script */
92
+ const __vue_script__ = script;
93
+
94
+ /* template */
95
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('gl-modal',{attrs:{"visible":_vm.visible,"modal-id":"agentic-tool-rejection-modal","title":_vm.$options.i18n.TITLE,"action-primary":_vm.submitAction,"action-secondary":_vm.backAction,"no-close-on-backdrop":true,"no-close-on-esc":true,"data-testid":"agentic-tool-rejection-modal"},on:{"primary":_vm.handleSubmit,"secondary":_vm.handleBack,"hide":_vm.handleModalHide}},[_c('p',{staticClass:"gl-mb-3",attrs:{"data-testid":"rejection-description"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.DESCRIPTION)+"\n ")]),_vm._v(" "),_c('gl-form-group',{staticClass:"gl-mb-0",attrs:{"label":_vm.$options.i18n.LABEL,"label-for":"rejection-message","optional":true}},[_c('gl-form-textarea',{attrs:{"id":"rejection-message","placeholder":_vm.$options.i18n.PLACEHOLDER,"rows":3,"max-rows":6,"resize":false,"data-testid":"rejection-message-input","autofocus":""},model:{value:(_vm.rejectionMessage),callback:function ($$v) {_vm.rejectionMessage=$$v;},expression:"rejectionMessage"}})],1)],1)};
96
+ var __vue_staticRenderFns__ = [];
97
+
98
+ /* style */
99
+ const __vue_inject_styles__ = undefined;
100
+ /* scoped */
101
+ const __vue_scope_id__ = undefined;
102
+ /* module identifier */
103
+ const __vue_module_identifier__ = undefined;
104
+ /* functional template */
105
+ const __vue_is_functional_template__ = false;
106
+ /* style inject */
107
+
108
+ /* style inject SSR */
109
+
110
+ /* style inject shadow dom */
111
+
112
+
113
+
114
+ const __vue_component__ = __vue_normalize__(
115
+ { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
116
+ __vue_inject_styles__,
117
+ __vue_script__,
118
+ __vue_scope_id__,
119
+ __vue_is_functional_template__,
120
+ __vue_module_identifier__,
121
+ false,
122
+ undefined,
123
+ undefined,
124
+ undefined
125
+ );
126
+
127
+ export default __vue_component__;
128
+ export { i18n };
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as InputRequestedMessage } from './components/chat/components/d
12
12
  export { default as SystemMessage } from './components/chat/components/duo_chat_message/message_types/message_tool';
13
13
  export { default as WorkflowEndMessage } from './components/chat/components/duo_chat_message/message_types/message_workflow_end';
14
14
  export { default as AgenticDuoChat } from './components/agentic_chat/agentic_duo_chat';
15
+ export { default as AgenticToolApprovalFlow } from './components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow';
15
16
  export { default as DuoChatContextItemDetailsModal } from './components/chat/components/duo_chat_context/duo_chat_context_item_details_modal/duo_chat_context_item_details_modal';
16
17
  export { default as DuoChatContextItemMenu } from './components/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu';
17
18
  export { default as DuoChatContextItemPopover } from './components/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/duo-ui",
3
- "version": "8.21.0",
3
+ "version": "8.22.0",
4
4
  "description": "Duo UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -97,7 +97,7 @@
97
97
  "@babel/plugin-proposal-optional-chaining": "^7.21.0",
98
98
  "@babel/preset-env": "^7.27.2",
99
99
  "@babel/preset-react": "^7.27.1",
100
- "@gitlab/eslint-plugin": "20.7.1",
100
+ "@gitlab/eslint-plugin": "21.0.0",
101
101
  "@gitlab/fonts": "^1.3.0",
102
102
  "@gitlab/stylelint-config": "6.2.2",
103
103
  "@gitlab/svgs": "^3.135.0",
@@ -155,8 +155,8 @@
155
155
  "module-alias": "^2.2.2",
156
156
  "npm-run-all": "^4.1.5",
157
157
  "pikaday": "^1.8.0",
158
- "playwright": "^1.52.0",
159
- "playwright-core": "^1.52.0",
158
+ "playwright": "^1.53.0",
159
+ "playwright-core": "^1.53.0",
160
160
  "plop": "^2.5.4",
161
161
  "postcss": "8.4.28",
162
162
  "postcss-loader": "^7.0.2",
@@ -0,0 +1,18 @@
1
+ The component provides a complete tool approval flow for Agentic Duo Chat.
2
+
3
+ It orchestrates a two-step approval process where users can approve tool execution
4
+ or deny it with an optional rejection reason.
5
+
6
+ ## Usage
7
+
8
+ To use the component, import it and add it to the `template` part of your consumer component.
9
+
10
+ ```html
11
+ <agentic-tool-approval-flow
12
+ :visible="showApproval"
13
+ :tool-details="toolDetails"
14
+ :description="customDescription"
15
+ @approve="handleApprove"
16
+ @deny="handleDeny"
17
+ />
18
+ ```
@@ -0,0 +1,119 @@
1
+ <script>
2
+ import { translate } from '../../../../utils/i18n';
3
+ import AgenticToolApprovalModal from './agentic_tool_approval_modal/agentic_tool_approval_modal.vue';
4
+ import AgenticToolRejectionModal from './agentic_tool_rejection_modal/agentic_tool_rejection_modal.vue';
5
+
6
+ export const i18n = {
7
+ DEFAULT_DESCRIPTION: translate(
8
+ 'AgenticToolApprovalFlow.defaultDescription',
9
+ 'Duo Workflow wants to execute a tool. Do you want to proceed?'
10
+ ),
11
+ };
12
+
13
+ const MODAL_TYPES = {
14
+ APPROVAL: 'approval',
15
+ REJECTION: 'rejection',
16
+ };
17
+
18
+ export default {
19
+ name: 'AgenticToolApprovalFlow',
20
+ components: {
21
+ AgenticToolApprovalModal,
22
+ AgenticToolRejectionModal,
23
+ },
24
+ props: {
25
+ /**
26
+ * Controls the visibility of the approval flow
27
+ */
28
+ visible: {
29
+ type: Boolean,
30
+ required: true,
31
+ },
32
+ /**
33
+ * Details about the tool to be executed
34
+ * @property {string} name - The name of the tool
35
+ * @property {Object} parameters - The parameters to be passed to the tool
36
+ */
37
+ toolDetails: {
38
+ type: Object,
39
+ required: false,
40
+ default: () => ({}),
41
+ validator: (value) => {
42
+ return typeof value === 'object' && value !== null;
43
+ },
44
+ },
45
+ /**
46
+ * Custom description text for the approval request
47
+ */
48
+ description: {
49
+ type: String,
50
+ required: false,
51
+ default: i18n.DEFAULT_DESCRIPTION,
52
+ },
53
+ },
54
+ data() {
55
+ return {
56
+ currentModal: MODAL_TYPES.APPROVAL,
57
+ };
58
+ },
59
+ computed: {
60
+ showApprovalModal() {
61
+ return this.visible && this.currentModal === MODAL_TYPES.APPROVAL;
62
+ },
63
+ showRejectionModal() {
64
+ return this.visible && this.currentModal === MODAL_TYPES.REJECTION;
65
+ },
66
+ },
67
+ watch: {
68
+ visible(newVal) {
69
+ if (!newVal) {
70
+ this.currentModal = MODAL_TYPES.APPROVAL;
71
+ }
72
+ },
73
+ },
74
+ methods: {
75
+ handleApprove() {
76
+ /**
77
+ * Emitted when the user approves the tool execution
78
+ */
79
+ this.$emit('approve');
80
+ },
81
+ handleInitialDeny() {
82
+ this.currentModal = MODAL_TYPES.REJECTION;
83
+ },
84
+ handleForceDeny() {
85
+ this.$emit('deny', null);
86
+ },
87
+ handleRejectionSubmit(message) {
88
+ /**
89
+ * Emitted when the user denies the tool execution with an optional reason
90
+ * @param {string|null} reason - The rejection reason provided by the user
91
+ */
92
+ this.$emit('deny', message);
93
+ },
94
+ handleBack() {
95
+ this.currentModal = MODAL_TYPES.APPROVAL;
96
+ },
97
+ },
98
+ i18n,
99
+ };
100
+ </script>
101
+
102
+ <template>
103
+ <div data-testid="agentic-tool-approval-flow">
104
+ <agentic-tool-approval-modal
105
+ :visible="showApprovalModal"
106
+ :tool-details="toolDetails"
107
+ :description="description"
108
+ @approve="handleApprove"
109
+ @deny="handleInitialDeny"
110
+ @deny-force="handleForceDeny"
111
+ />
112
+
113
+ <agentic-tool-rejection-modal
114
+ :visible="showRejectionModal"
115
+ @submit="handleRejectionSubmit"
116
+ @back="handleBack"
117
+ />
118
+ </div>
119
+ </template>
@@ -0,0 +1,138 @@
1
+ <script>
2
+ import { GlModal } from '@gitlab/ui';
3
+ import { translate } from '../../../../../utils/i18n';
4
+
5
+ export const i18n = {
6
+ TITLE: translate('AgenticToolApprovalModal.title', 'Tool Approval Required'),
7
+ DEFAULT_DESCRIPTION: translate(
8
+ 'AgenticToolApprovalModal.defaultDescription',
9
+ 'Duo Workflow wants to execute a tool. Do you want to proceed?'
10
+ ),
11
+ TOOL_LABEL: translate('AgenticToolApprovalModal.toolLabel', 'Tool:'),
12
+ TOOL_UNKNOWN: translate('AgenticToolApprovalModal.toolUnknown', 'Unknown'),
13
+ APPROVE_TEXT: translate('AgenticToolApprovalModal.approveText', 'Approve'),
14
+ DENY_TEXT: translate('AgenticToolApprovalModal.denyText', 'Deny'),
15
+ };
16
+
17
+ export default {
18
+ name: 'AgenticToolApprovalModal',
19
+ components: {
20
+ GlModal,
21
+ },
22
+ props: {
23
+ /**
24
+ * Controls the visibility of the modal
25
+ */
26
+ visible: {
27
+ type: Boolean,
28
+ required: true,
29
+ },
30
+ /**
31
+ * Details about the tool to be executed
32
+ * @property {string} name - The name of the tool
33
+ * @property {Object} parameters - The parameters to be passed to the tool
34
+ */
35
+ toolDetails: {
36
+ type: Object,
37
+ required: false,
38
+ default: () => ({}),
39
+ validator: (value) => {
40
+ return typeof value === 'object' && value !== null;
41
+ },
42
+ },
43
+ /**
44
+ * Custom description text for the approval request
45
+ */
46
+ description: {
47
+ type: String,
48
+ required: false,
49
+ default: i18n.DEFAULT_DESCRIPTION,
50
+ },
51
+ },
52
+ computed: {
53
+ approveAction() {
54
+ return {
55
+ text: this.$options.i18n.APPROVE_TEXT,
56
+ attributes: {
57
+ variant: 'confirm',
58
+ icon: 'play',
59
+ size: 'small',
60
+ 'data-testid': 'approve-tool',
61
+ },
62
+ };
63
+ },
64
+ denyAction() {
65
+ return {
66
+ text: this.$options.i18n.DENY_TEXT,
67
+ attributes: {
68
+ variant: 'danger',
69
+ icon: 'cancel',
70
+ size: 'small',
71
+ 'data-testid': 'deny-tool',
72
+ },
73
+ };
74
+ },
75
+ toolName() {
76
+ return this.toolDetails?.name || this.$options.i18n.TOOL_UNKNOWN;
77
+ },
78
+ },
79
+ methods: {
80
+ handleApprove() {
81
+ /**
82
+ * Emitted when the user approves the tool execution
83
+ */
84
+ this.$emit('approve');
85
+ },
86
+ handleDeny() {
87
+ /**
88
+ * Emitted when the user denies the tool execution
89
+ */
90
+ this.$emit('deny');
91
+ },
92
+ handleModalHide(event) {
93
+ // If the modal is being hidden by the X button or ESC key,
94
+ // treat it as a deny-force action
95
+ if (event.trigger !== 'ok' && event.trigger !== 'cancel') {
96
+ this.$emit('deny-force');
97
+ }
98
+ },
99
+ },
100
+ i18n,
101
+ };
102
+ </script>
103
+
104
+ <template>
105
+ <gl-modal
106
+ :visible="visible"
107
+ modal-id="agentic-tool-approval-modal"
108
+ :title="$options.i18n.TITLE"
109
+ :action-primary="approveAction"
110
+ :action-cancel="denyAction"
111
+ :no-close-on-backdrop="true"
112
+ :no-close-on-esc="true"
113
+ data-testid="agentic-tool-approval-modal"
114
+ @primary="handleApprove"
115
+ @canceled="handleDeny"
116
+ @hide="handleModalHide"
117
+ >
118
+ <div class="gl-mb-4">
119
+ <p class="gl-mb-3" data-testid="approval-description">
120
+ {{ description }}
121
+ </p>
122
+
123
+ <div
124
+ v-if="toolDetails"
125
+ class="gl-bg-gray-10 gl-p-3 gl-rounded-base gl-mb-3"
126
+ data-testid="tool-details"
127
+ >
128
+ <strong>{{ $options.i18n.TOOL_LABEL }} {{ toolName }}</strong>
129
+ <pre
130
+ v-if="toolDetails.parameters"
131
+ class="gl-mt-2 gl-mb-0 gl-text-inherit"
132
+ data-testid="tool-parameters"
133
+ >{{ JSON.stringify(toolDetails.parameters, null, 2) }}</pre
134
+ >
135
+ </div>
136
+ </div>
137
+ </gl-modal>
138
+ </template>
@@ -0,0 +1,134 @@
1
+ <script>
2
+ import { GlModal, GlFormTextarea, GlFormGroup } from '@gitlab/ui';
3
+ import { translate } from '../../../../../utils/i18n';
4
+
5
+ export const i18n = {
6
+ TITLE: translate('AgenticToolRejectionModal.title', 'Provide Rejection Reason'),
7
+ DESCRIPTION: translate(
8
+ 'AgenticToolRejectionModal.description',
9
+ 'You are about to reject the tool execution. Would you like to provide a reason?'
10
+ ),
11
+ LABEL: translate('AgenticToolRejectionModal.label', 'Rejection reason'),
12
+ PLACEHOLDER: translate(
13
+ 'AgenticToolRejectionModal.placeholder',
14
+ "Explain why you're rejecting this tool execution..."
15
+ ),
16
+ SUBMIT_TEXT: translate('AgenticToolRejectionModal.submitText', 'Submit Rejection'),
17
+ BACK_TEXT: translate('AgenticToolRejectionModal.backText', 'Back'),
18
+ };
19
+
20
+ export default {
21
+ name: 'AgenticToolRejectionModal',
22
+ components: {
23
+ GlModal,
24
+ GlFormTextarea,
25
+ GlFormGroup,
26
+ },
27
+ props: {
28
+ /**
29
+ * Controls the visibility of the modal
30
+ */
31
+ visible: {
32
+ type: Boolean,
33
+ required: true,
34
+ },
35
+ },
36
+ data() {
37
+ return {
38
+ rejectionMessage: '',
39
+ };
40
+ },
41
+ computed: {
42
+ submitAction() {
43
+ return {
44
+ text: this.$options.i18n.SUBMIT_TEXT,
45
+ attributes: {
46
+ variant: 'danger',
47
+ icon: 'cancel',
48
+ size: 'small',
49
+ 'data-testid': 'submit-rejection',
50
+ },
51
+ };
52
+ },
53
+ backAction() {
54
+ return {
55
+ text: this.$options.i18n.BACK_TEXT,
56
+ attributes: {
57
+ variant: 'default',
58
+ icon: 'chevron-left',
59
+ size: 'small',
60
+ 'data-testid': 'back-button',
61
+ },
62
+ };
63
+ },
64
+ },
65
+ watch: {
66
+ visible(newVal) {
67
+ if (!newVal) {
68
+ this.rejectionMessage = '';
69
+ }
70
+ },
71
+ },
72
+ methods: {
73
+ handleSubmit() {
74
+ /**
75
+ * Emitted when the user submits the rejection reason
76
+ * @param {string|null} reason - The rejection reason provided by the user, or null if empty
77
+ */
78
+ this.$emit('submit', this.rejectionMessage || null);
79
+ this.rejectionMessage = '';
80
+ },
81
+ handleBack() {
82
+ /**
83
+ * Emitted when the user clicks the back button to return to the approval modal
84
+ */
85
+ this.$emit('back');
86
+ },
87
+ handleModalHide(event) {
88
+ // If the modal is being hidden by the X button or ESC key,
89
+ // treat it as a deny-force action
90
+ if (event.trigger !== 'ok' && event.trigger !== 'cancel') {
91
+ this.handleBack();
92
+ }
93
+ },
94
+ },
95
+ i18n,
96
+ };
97
+ </script>
98
+
99
+ <template>
100
+ <gl-modal
101
+ :visible="visible"
102
+ modal-id="agentic-tool-rejection-modal"
103
+ :title="$options.i18n.TITLE"
104
+ :action-primary="submitAction"
105
+ :action-secondary="backAction"
106
+ :no-close-on-backdrop="true"
107
+ :no-close-on-esc="true"
108
+ data-testid="agentic-tool-rejection-modal"
109
+ @primary="handleSubmit"
110
+ @secondary="handleBack"
111
+ @hide="handleModalHide"
112
+ >
113
+ <p class="gl-mb-3" data-testid="rejection-description">
114
+ {{ $options.i18n.DESCRIPTION }}
115
+ </p>
116
+ <gl-form-group
117
+ :label="$options.i18n.LABEL"
118
+ label-for="rejection-message"
119
+ :optional="true"
120
+ class="gl-mb-0"
121
+ >
122
+ <gl-form-textarea
123
+ id="rejection-message"
124
+ v-model="rejectionMessage"
125
+ :placeholder="$options.i18n.PLACEHOLDER"
126
+ :rows="3"
127
+ :max-rows="6"
128
+ :resize="false"
129
+ data-testid="rejection-message-input"
130
+ autofocus
131
+ />
132
+ </gl-form-group>
133
+ </gl-modal>
134
+ </template>
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ export { default as WorkflowEndMessage } from './components/chat/components/duo_
20
20
 
21
21
  // Agentic Duo Chat component
22
22
  export { default as AgenticDuoChat } from './components/agentic_chat/agentic_duo_chat.vue';
23
+ export { default as AgenticToolApprovalFlow } from './components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow.vue';
23
24
 
24
25
  // Duo Chat Context components
25
26
  export { default as DuoChatContextItemDetailsModal } from './components/chat/components/duo_chat_context/duo_chat_context_item_details_modal/duo_chat_context_item_details_modal.vue';
package/translations.js CHANGED
@@ -15,6 +15,22 @@ export default {
15
15
  'AgenticDuoChat.chatPromptPlaceholderDefault': 'GitLab Duo Chat',
16
16
  'AgenticDuoChat.chatPromptPlaceholderWithCommands': 'Type /help to learn more',
17
17
  'AgenticDuoChat.chatSubmitLabel': 'Send chat message.',
18
+ 'AgenticToolApprovalFlow.defaultDescription':
19
+ 'Duo Workflow wants to execute a tool. Do you want to proceed?',
20
+ 'AgenticToolApprovalModal.approveText': 'Approve',
21
+ 'AgenticToolApprovalModal.defaultDescription':
22
+ 'Duo Workflow wants to execute a tool. Do you want to proceed?',
23
+ 'AgenticToolApprovalModal.denyText': 'Deny',
24
+ 'AgenticToolApprovalModal.title': 'Tool Approval Required',
25
+ 'AgenticToolApprovalModal.toolLabel': 'Tool:',
26
+ 'AgenticToolApprovalModal.toolUnknown': 'Unknown',
27
+ 'AgenticToolRejectionModal.backText': 'Back',
28
+ 'AgenticToolRejectionModal.description':
29
+ 'You are about to reject the tool execution. Would you like to provide a reason?',
30
+ 'AgenticToolRejectionModal.label': 'Rejection reason',
31
+ 'AgenticToolRejectionModal.placeholder': "Explain why you're rejecting this tool execution...",
32
+ 'AgenticToolRejectionModal.submitText': 'Submit Rejection',
33
+ 'AgenticToolRejectionModal.title': 'Provide Rejection Reason',
18
34
  'DuoChat.chatBackLabel': 'Back to history',
19
35
  'DuoChat.chatBackToChatToolTip': 'Back to chat',
20
36
  'DuoChat.chatCancelLabel': 'Cancel',