@gitlab/duo-ui 14.3.0 → 15.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/components/chat/components/duo_chat_header/duo_chat_header.js +2 -1
- package/dist/components/chat/components/duo_chat_header/web_duo_chat_header.js +4 -11
- package/dist/components/chat/components/duo_chat_message/copy_code_element.js +4 -14
- package/dist/components/chat/components/duo_chat_message/duo_chat_message.js +12 -13
- package/dist/components/chat/components/duo_chat_message_tool_approval/components/pre_block.js +2 -21
- package/dist/components/chat/components/utils.js +56 -0
- package/package.json +1 -1
- package/src/components/chat/components/duo_chat_header/duo_chat_header.vue +3 -1
- package/src/components/chat/components/duo_chat_header/web_duo_chat_header.vue +3 -42
- package/src/components/chat/components/duo_chat_message/copy_code_element.js +4 -17
- package/src/components/chat/components/duo_chat_message/duo_chat_message.vue +13 -13
- package/src/components/chat/components/duo_chat_message_tool_approval/components/pre_block.vue +2 -20
- package/src/components/chat/components/utils.js +60 -0
- package/translations.js +0 -2
- package/dist/components/chat/components/duo_chat_message/utils.js +0 -21
- package/src/components/chat/components/duo_chat_message/utils.js +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [15.0.1](https://gitlab.com/gitlab-org/duo-ui/compare/v15.0.0...v15.0.1) (2025-11-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Use a generic clipboard copy action ([b1eb7a3](https://gitlab.com/gitlab-org/duo-ui/commit/b1eb7a3b5d56c42346a569bbc14740aeb8a73fb4))
|
|
7
|
+
|
|
8
|
+
# [15.0.0](https://gitlab.com/gitlab-org/duo-ui/compare/v14.3.0...v15.0.0) (2025-11-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **DuoChat:** Remove new chat button ([5e9944f](https://gitlab.com/gitlab-org/duo-ui/commit/5e9944f5dfbb6963f27152636c727908bf0228d8))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* **DuoChat:** Removes New Chat button for web header as the
|
|
19
|
+
button is now toggles from the navigation siderail
|
|
20
|
+
|
|
1
21
|
# [14.3.0](https://gitlab.com/gitlab-org/duo-ui/compare/v14.2.0...v14.3.0) (2025-11-20)
|
|
2
22
|
|
|
3
23
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Vue from 'vue';
|
|
2
2
|
import { GlToast, GlAlert, GlButton, GlExperimentBadge, GlDisclosureDropdown, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
|
|
3
3
|
import { translate, sprintf } from '../../../../utils/i18n';
|
|
4
|
+
import { copyToClipboard } from '../utils';
|
|
4
5
|
import { VIEW_TYPES } from './constants';
|
|
5
6
|
import DuoChatHeaderAgentItem from './duo_chat_header_agent_item/duo_chat_header_agent_item';
|
|
6
7
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
@@ -133,7 +134,7 @@ var script = {
|
|
|
133
134
|
},
|
|
134
135
|
async copySessionIdToClipboard() {
|
|
135
136
|
try {
|
|
136
|
-
await
|
|
137
|
+
await copyToClipboard(this.sessionId, this.$el);
|
|
137
138
|
this.$toast.show('Session ID copied to clipboard');
|
|
138
139
|
} catch {
|
|
139
140
|
this.$toast.show('Could not copy session ID');
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import Vue from 'vue';
|
|
2
2
|
import { GlToast, GlAlert, GlAvatar, GlButton, GlDisclosureDropdown, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
|
|
3
3
|
import { translate, sprintf } from '../../../../utils/i18n';
|
|
4
|
-
import
|
|
4
|
+
import { copyToClipboard } from '../utils';
|
|
5
5
|
import { VIEW_TYPES } from './constants';
|
|
6
6
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
7
7
|
|
|
8
8
|
const i18n = {
|
|
9
9
|
CHAT_CLOSE_LABEL: translate('WebDuoChat.closeChatHeaderLabel', 'Close chat'),
|
|
10
|
-
CHAT_NEW_LABEL: translate('WebDuoChat.chatNewLabel', 'New chat'),
|
|
11
|
-
CHAT_NEW_TOOLTIP: translate('WebDuoChat.chatNewToolTip', 'New chat'),
|
|
12
10
|
CHAT_BACK_TO_CHAT_TOOLTIP: translate('WebDuoChat.chatBackToChatToolTip', 'Back to chat'),
|
|
13
11
|
CHAT_TITLE: translate('WebDuoChat.chatTitle', 'GitLab Duo Chat'),
|
|
14
12
|
CHAT_DROPDOWN_MORE_OPTIONS: translate('WebDuoChat.chatDropdownMoreOptions', 'More options'),
|
|
@@ -23,8 +21,7 @@ var script = {
|
|
|
23
21
|
GlAlert,
|
|
24
22
|
GlAvatar,
|
|
25
23
|
GlButton,
|
|
26
|
-
GlDisclosureDropdown
|
|
27
|
-
DuoChatHeaderAgentItem
|
|
24
|
+
GlDisclosureDropdown
|
|
28
25
|
},
|
|
29
26
|
directives: {
|
|
30
27
|
SafeHtml: GlSafeHtmlDirective,
|
|
@@ -144,7 +141,7 @@ var script = {
|
|
|
144
141
|
},
|
|
145
142
|
async copySessionIdToClipboard() {
|
|
146
143
|
try {
|
|
147
|
-
await
|
|
144
|
+
await copyToClipboard(this.sessionId, this.$el);
|
|
148
145
|
this.$toast.show('Session ID copied to clipboard');
|
|
149
146
|
} catch {
|
|
150
147
|
this.$toast.show('Could not copy session ID');
|
|
@@ -158,11 +155,7 @@ var script = {
|
|
|
158
155
|
const __vue_script__ = script;
|
|
159
156
|
|
|
160
157
|
/* template */
|
|
161
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('header',{staticClass:"gl-shrink-0 gl-p-0",attrs:{"data-testid":"chat-header"}},[(!_vm.showStudioHeader)?_c('div',{staticClass:"gl-flex gl-w-full gl-items-center gl-px-4 gl-py-3",attrs:{"data-testid":"chat-top-header"}},[(_vm.subtitle)?_c('h4',{staticClass:"gl-mb-0 gl-max-w-17/20 gl-flex-1 gl-shrink-0 gl-overflow-hidden gl-truncate gl-text-ellipsis gl-whitespace-nowrap gl-pr-3 gl-text-sm gl-font-normal gl-text-subtle",attrs:{"data-testid":"chat-subtitle"}},[_vm._v("\n "+_vm._s(_vm.subtitle)+"\n ")]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-ml-auto gl-flex gl-gap-3"},[(_vm.sessionId)?_c('gl-disclosure-dropdown',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:(_vm.showSessionDropdownTooltip),expression:"showSessionDropdownTooltip"}],attrs:{"icon":"ellipsis_v","category":"tertiary","text-sr-only":"","size":"small","toggle-text":_vm.$options.i18n.CHAT_DROPDOWN_MORE_OPTIONS,"items":_vm.sessionIdItems,"no-caret":""},on:{"shown":_vm.showSessionDropdown,"hidden":_vm.hideSessionDropdown}}):_vm._e(),_vm._v(" "),_c('gl-button',{attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":function($event){return _vm.$emit('close')}}})],1)]):_vm._e(),_vm._v(" "),(_vm.showSubheader)?_c('div',{staticClass:"drawer-title gl-flex gl-items-center gl-justify-start gl-gap-4 gl-px-4 gl-py-3",class:{ 'gl-border-t': !_vm.showStudioHeader, 'gl-border-b': _vm.showStudioHeader },attrs:{"data-testid":"chat-subheader"}},[_c('div',{staticClass:"gl-flex gl-grow gl-gap-3"},[_c('gl-avatar',{staticClass:"gl-shrink-0",attrs:{"size":32,"entity-name":_vm.title,"shape":"circle"}}),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-flex-col gl-justify-center"},[_c('h3',{staticClass:"gl-my-0 gl-line-clamp-1 gl-text-[0.875rem] gl-text-default"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),(_vm.agentHandler)?_c('p',{staticClass:"gl-my-0 gl-text-[0.75rem] gl-text-subtle"},[_vm._v("\n "+_vm._s(_vm.agentHandler)+"\n ")]):_vm._e()])],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-gap-3"},[(
|
|
162
|
-
_vm.isMultithreaded && (_vm.activeThreadId || _vm.currentView === _vm.VIEW_TYPES.LIST || _vm.hasManyAgents)
|
|
163
|
-
)?[(_vm.hasManyAgents)?_c('gl-disclosure-dropdown',{attrs:{"title":_vm.$options.i18n.CHAT_NEW_TOOLTIP,"toggle-text":_vm.$options.i18n.CHAT_NEW_TOOLTIP,"items":_vm.agents,"data-testid":"chat-new-button","variant":"confirm","category":"tertiary","size":"small","icon":"duo-chat-new","text-sr-only":"","aria-label":_vm.$options.i18n.CHAT_NEW_LABEL,"no-caret":""},on:{"action":_vm.startNewChat},scopedSlots:_vm._u([{key:"list-item",fn:function(ref){
|
|
164
|
-
var item = ref.item;
|
|
165
|
-
return [_c('duo-chat-header-agent-item',{attrs:{"agent":item}})]}}],null,false,1406306174)}):_c('gl-button',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip"}],attrs:{"title":_vm.$options.i18n.CHAT_NEW_TOOLTIP,"data-testid":"chat-new-button","variant":"confirm","category":"tertiary","size":"small","icon":"duo-chat-new","aria-label":_vm.$options.i18n.CHAT_NEW_LABEL},on:{"click":_vm.startNewChat}})]:_vm._e(),_vm._v(" "),(_vm.isMultithreaded && _vm.activeThreadId && _vm.currentView === _vm.VIEW_TYPES.LIST)?_c('gl-button',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip"}],attrs:{"title":_vm.$options.i18n.CHAT_BACK_TO_CHAT_TOOLTIP,"data-testid":"go-back-to-chat-button","category":"tertiary","size":"small","icon":"go-back","aria-label":_vm.$options.i18n.CHAT_BACK_TO_CHAT_TOOLTIP},on:{"click":function($event){return _vm.$emit('go-back-to-chat')}}}):_vm._e()],2)]):_vm._e(),_vm._v(" "),_vm._t("subheader"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"!gl-pl-9",attrs:{"dismissible":false,"variant":"danger","role":"alert","data-testid":"chat-error"}},[_c('span',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(_vm.error),expression:"error"}]})]):_vm._e()],2)};
|
|
158
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('header',{staticClass:"gl-shrink-0 gl-p-0",attrs:{"data-testid":"chat-header"}},[(!_vm.showStudioHeader)?_c('div',{staticClass:"gl-flex gl-w-full gl-items-center gl-px-4 gl-py-3",attrs:{"data-testid":"chat-top-header"}},[(_vm.subtitle)?_c('h4',{staticClass:"gl-mb-0 gl-max-w-17/20 gl-flex-1 gl-shrink-0 gl-overflow-hidden gl-truncate gl-text-ellipsis gl-whitespace-nowrap gl-pr-3 gl-text-sm gl-font-normal gl-text-subtle",attrs:{"data-testid":"chat-subtitle"}},[_vm._v("\n "+_vm._s(_vm.subtitle)+"\n ")]):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-ml-auto gl-flex gl-gap-3"},[(_vm.sessionId)?_c('gl-disclosure-dropdown',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip",value:(_vm.showSessionDropdownTooltip),expression:"showSessionDropdownTooltip"}],attrs:{"icon":"ellipsis_v","category":"tertiary","text-sr-only":"","size":"small","toggle-text":_vm.$options.i18n.CHAT_DROPDOWN_MORE_OPTIONS,"items":_vm.sessionIdItems,"no-caret":""},on:{"shown":_vm.showSessionDropdown,"hidden":_vm.hideSessionDropdown}}):_vm._e(),_vm._v(" "),_c('gl-button',{attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":function($event){return _vm.$emit('close')}}})],1)]):_vm._e(),_vm._v(" "),(_vm.showSubheader)?_c('div',{staticClass:"drawer-title gl-flex gl-items-center gl-justify-start gl-gap-4 gl-px-4 gl-py-3",class:{ 'gl-border-t': !_vm.showStudioHeader, 'gl-border-b': _vm.showStudioHeader },attrs:{"data-testid":"chat-subheader"}},[_c('div',{staticClass:"gl-flex gl-grow gl-gap-3"},[_c('gl-avatar',{staticClass:"gl-shrink-0",attrs:{"size":32,"entity-name":_vm.title,"shape":"circle"}}),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-flex-col gl-justify-center"},[_c('h3',{staticClass:"gl-my-0 gl-line-clamp-1 gl-text-[0.875rem] gl-text-default"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),(_vm.agentHandler)?_c('p',{staticClass:"gl-my-0 gl-text-[0.75rem] gl-text-subtle"},[_vm._v("\n "+_vm._s(_vm.agentHandler)+"\n ")]):_vm._e()])],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-gap-3"},[(_vm.isMultithreaded && _vm.activeThreadId && _vm.currentView === _vm.VIEW_TYPES.LIST)?_c('gl-button',{directives:[{name:"gl-tooltip",rawName:"v-gl-tooltip"}],attrs:{"title":_vm.$options.i18n.CHAT_BACK_TO_CHAT_TOOLTIP,"data-testid":"go-back-to-chat-button","category":"tertiary","size":"small","icon":"go-back","aria-label":_vm.$options.i18n.CHAT_BACK_TO_CHAT_TOOLTIP},on:{"click":function($event){return _vm.$emit('go-back-to-chat')}}}):_vm._e()],1)]):_vm._e(),_vm._v(" "),_vm._t("subheader"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"!gl-pl-9",attrs:{"dismissible":false,"variant":"danger","role":"alert","data-testid":"chat-error"}},[_c('span',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(_vm.error),expression:"error"}]})]):_vm._e()],2)};
|
|
166
159
|
var __vue_staticRenderFns__ = [];
|
|
167
160
|
|
|
168
161
|
/* style */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { copyToClipboard } from '../utils';
|
|
1
2
|
import { createButton } from './buttons_utils';
|
|
2
3
|
import { createTooltip } from './tooltips_utils';
|
|
3
|
-
import { checkClipboardPermissions } from './utils';
|
|
4
4
|
|
|
5
5
|
function _defineProperty(e, r, t) {
|
|
6
6
|
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
@@ -46,21 +46,11 @@ class CopyCodeElement extends HTMLElement {
|
|
|
46
46
|
// not present in the DOM.
|
|
47
47
|
const codeElement = this.getCodeElement();
|
|
48
48
|
const textToCopy = codeElement.innerText;
|
|
49
|
-
const hasClipboardPermission = await checkClipboardPermissions();
|
|
50
49
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
cancelable: true,
|
|
54
|
-
detail: {
|
|
55
|
-
code: textToCopy
|
|
56
|
-
}
|
|
57
|
-
}));
|
|
58
|
-
if (hasClipboardPermission) {
|
|
59
|
-
await navigator.clipboard.writeText(textToCopy);
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {
|
|
50
|
+
await copyToClipboard(textToCopy, codeElement);
|
|
51
|
+
} catch (error) {
|
|
62
52
|
// eslint-disable-next-line no-console
|
|
63
|
-
console.warn('Failed to copy
|
|
53
|
+
console.warn('Failed to copy to clipboard:', error);
|
|
64
54
|
}
|
|
65
55
|
});
|
|
66
56
|
}
|
|
@@ -5,10 +5,10 @@ import DuoChatContextItemSelections from '../duo_chat_context/duo_chat_context_i
|
|
|
5
5
|
import { SELECTED_CONTEXT_ITEMS_DEFAULT_COLLAPSED, MESSAGE_MODEL_ROLES } from '../../constants';
|
|
6
6
|
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources';
|
|
7
7
|
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
8
|
+
import { concatUntilEmpty, copyToClipboard } from '../utils';
|
|
8
9
|
import MessageFeedback from './message_feedback';
|
|
9
10
|
import { CopyCodeElement } from './copy_code_element';
|
|
10
11
|
import { InsertCodeSnippetElement } from './insert_code_snippet_element';
|
|
11
|
-
import { concatUntilEmpty, checkClipboardPermissions } from './utils';
|
|
12
12
|
import { DUO_CODE_SCRIM_BOTTOM_CLASS, DUO_CODE_SCRIM_OFFSET, DUO_CODE_SCRIM_TOP_CLASS } from './constants';
|
|
13
13
|
import MessageMap from './message_types/message_map';
|
|
14
14
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
@@ -170,7 +170,7 @@ var script = {
|
|
|
170
170
|
});
|
|
171
171
|
},
|
|
172
172
|
shouldShowCopyAction() {
|
|
173
|
-
return Boolean(
|
|
173
|
+
return Boolean(!this.isChunk && this.isAssistantMessage);
|
|
174
174
|
},
|
|
175
175
|
shouldShowFeedbackLink() {
|
|
176
176
|
return this.withFeedback && this.isNotChunkOrCancelled && this.isAssistantMessage;
|
|
@@ -273,18 +273,17 @@ var script = {
|
|
|
273
273
|
}
|
|
274
274
|
},
|
|
275
275
|
async copyMessage() {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
message: this.message.content
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
}
|
|
276
|
+
try {
|
|
277
|
+
await copyToClipboard(this.message.content, this.$el);
|
|
278
|
+
this.$emit('copy-message', {
|
|
279
|
+
detail: {
|
|
280
|
+
message: this.message.content
|
|
281
|
+
}
|
|
282
|
+
});
|
|
287
283
|
this.copied = true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// eslint-disable-next-line no-console
|
|
286
|
+
console.warn('Failed to copy message:', error);
|
|
288
287
|
}
|
|
289
288
|
},
|
|
290
289
|
onOpenFilePath(e) {
|
package/dist/components/chat/components/duo_chat_message_tool_approval/components/pre_block.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
|
|
2
2
|
import { translate } from '../../../../../utils/i18n';
|
|
3
|
-
import {
|
|
3
|
+
import { copyToClipboard } from '../../utils';
|
|
4
4
|
import { highlightElement } from '../services/highlight';
|
|
5
5
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
6
6
|
|
|
@@ -39,26 +39,7 @@ var script = {
|
|
|
39
39
|
async copyToClipboard() {
|
|
40
40
|
try {
|
|
41
41
|
const textToCopy = this.$refs.codeElement.textContent;
|
|
42
|
-
|
|
43
|
-
const event = new CustomEvent('copy-code-snippet', {
|
|
44
|
-
bubbles: true,
|
|
45
|
-
detail: {
|
|
46
|
-
code: textToCopy
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
// Emit custom event for VS Code and other environments
|
|
50
|
-
// This bubbles up to parent components that handle clipboard operations
|
|
51
|
-
this.$emit('copy-code-snippet', {
|
|
52
|
-
detail: {
|
|
53
|
-
code: textToCopy
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
this.$el.dispatchEvent(event);
|
|
57
|
-
|
|
58
|
-
// If we have clipboard permission, also copy directly
|
|
59
|
-
if (hasClipboardPermission) {
|
|
60
|
-
await navigator.clipboard.writeText(textToCopy);
|
|
61
|
-
}
|
|
42
|
+
await copyToClipboard(textToCopy, this.$el);
|
|
62
43
|
this.$emit('copied', this.title);
|
|
63
44
|
setTimeout(() => {
|
|
64
45
|
this.$refs.copyToClipboardButton.$el.blur();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const concatUntilEmpty = arr => {
|
|
2
|
+
if (!arr) return '';
|
|
3
|
+
let end = arr.findIndex(el => !el);
|
|
4
|
+
if (end < 0) end = arr.length;
|
|
5
|
+
return arr.slice(0, end).join('');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Programmatically copies a text string to the clipboard. Attempts to copy in both secure and non-secure contexts as well as VSCode.
|
|
10
|
+
*
|
|
11
|
+
* Accepts a container element. This helps ensure the text can get copied to the clipboard correctly in non-secure
|
|
12
|
+
* environments, the container should be active (such as a button in a modal) to ensure the content can be copied.
|
|
13
|
+
*
|
|
14
|
+
* @param {String} text - Text to copy
|
|
15
|
+
* @param {HTMLElement} el - element that emits event for VSCode, and contains the fallback textarea.
|
|
16
|
+
*/
|
|
17
|
+
const copyToClipboard = (textToCopy, el) => {
|
|
18
|
+
// Fist, emit a custom event for VS Code extensions and other environments
|
|
19
|
+
// This bubbles up to parent components that handle clipboard operations
|
|
20
|
+
el.dispatchEvent(new CustomEvent('copy-code-snippet', {
|
|
21
|
+
bubbles: true,
|
|
22
|
+
cancelable: true,
|
|
23
|
+
detail: {
|
|
24
|
+
code: textToCopy
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Second, for modern browsers try a simple clipboard.writeText (works on https and localhost)
|
|
29
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
30
|
+
return navigator.clipboard.writeText(textToCopy);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Third, try execCommand to copy from a dynamically created invisible textarea (for http and older browsers)
|
|
34
|
+
const textarea = document.createElement('textarea');
|
|
35
|
+
textarea.value = textToCopy;
|
|
36
|
+
textarea.style.position = 'absolute';
|
|
37
|
+
textarea.style.left = '-9999px'; // eslint-disable-line @gitlab/require-i18n-strings
|
|
38
|
+
textarea.style.top = '0';
|
|
39
|
+
textarea.setAttribute('readonly', ''); // prevent keyboard popup on mobile
|
|
40
|
+
|
|
41
|
+
// textarea must be in document to be selectable, but we add it to the button so it works in modals
|
|
42
|
+
el.appendChild(textarea);
|
|
43
|
+
textarea.select(); // for Safari
|
|
44
|
+
textarea.setSelectionRange(0, textarea.value.length); // for mobile devices
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const done = document.execCommand('copy');
|
|
48
|
+
el.removeChild(textarea);
|
|
49
|
+
return done ? Promise.resolve() : Promise.reject(new Error('Copy command failed'));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
el.removeChild(textarea);
|
|
52
|
+
return Promise.reject(err);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { concatUntilEmpty, copyToClipboard };
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
GlDisclosureDropdown,
|
|
11
11
|
} from '@gitlab/ui';
|
|
12
12
|
import { sprintf, translate } from '../../../../utils/i18n';
|
|
13
|
+
import { copyToClipboard } from '../utils';
|
|
13
14
|
import { VIEW_TYPES } from './constants';
|
|
14
15
|
import DuoChatHeaderAgentItem from './duo_chat_header_agent_item/duo_chat_header_agent_item.vue';
|
|
15
16
|
|
|
@@ -150,7 +151,8 @@ export default {
|
|
|
150
151
|
},
|
|
151
152
|
async copySessionIdToClipboard() {
|
|
152
153
|
try {
|
|
153
|
-
await
|
|
154
|
+
await copyToClipboard(this.sessionId, this.$el);
|
|
155
|
+
|
|
154
156
|
this.$toast.show('Session ID copied to clipboard');
|
|
155
157
|
} catch {
|
|
156
158
|
this.$toast.show('Could not copy session ID');
|
|
@@ -10,13 +10,11 @@ import {
|
|
|
10
10
|
GlDisclosureDropdown,
|
|
11
11
|
} from '@gitlab/ui';
|
|
12
12
|
import { sprintf, translate } from '../../../../utils/i18n';
|
|
13
|
-
import
|
|
13
|
+
import { copyToClipboard } from '../utils';
|
|
14
14
|
import { VIEW_TYPES } from './constants';
|
|
15
15
|
|
|
16
16
|
export const i18n = {
|
|
17
17
|
CHAT_CLOSE_LABEL: translate('WebDuoChat.closeChatHeaderLabel', 'Close chat'),
|
|
18
|
-
CHAT_NEW_LABEL: translate('WebDuoChat.chatNewLabel', 'New chat'),
|
|
19
|
-
CHAT_NEW_TOOLTIP: translate('WebDuoChat.chatNewToolTip', 'New chat'),
|
|
20
18
|
CHAT_BACK_TO_CHAT_TOOLTIP: translate('WebDuoChat.chatBackToChatToolTip', 'Back to chat'),
|
|
21
19
|
CHAT_TITLE: translate('WebDuoChat.chatTitle', 'GitLab Duo Chat'),
|
|
22
20
|
CHAT_DROPDOWN_MORE_OPTIONS: translate('WebDuoChat.chatDropdownMoreOptions', 'More options'),
|
|
@@ -41,7 +39,6 @@ export default {
|
|
|
41
39
|
GlAvatar,
|
|
42
40
|
GlButton,
|
|
43
41
|
GlDisclosureDropdown,
|
|
44
|
-
DuoChatHeaderAgentItem,
|
|
45
42
|
},
|
|
46
43
|
directives: {
|
|
47
44
|
SafeHtml,
|
|
@@ -161,7 +158,8 @@ export default {
|
|
|
161
158
|
},
|
|
162
159
|
async copySessionIdToClipboard() {
|
|
163
160
|
try {
|
|
164
|
-
await
|
|
161
|
+
await copyToClipboard(this.sessionId, this.$el);
|
|
162
|
+
|
|
165
163
|
this.$toast.show('Session ID copied to clipboard');
|
|
166
164
|
} catch {
|
|
167
165
|
this.$toast.show('Could not copy session ID');
|
|
@@ -228,43 +226,6 @@ export default {
|
|
|
228
226
|
</div>
|
|
229
227
|
|
|
230
228
|
<div class="gl-flex gl-gap-3">
|
|
231
|
-
<template
|
|
232
|
-
v-if="
|
|
233
|
-
isMultithreaded && (activeThreadId || currentView === VIEW_TYPES.LIST || hasManyAgents)
|
|
234
|
-
"
|
|
235
|
-
>
|
|
236
|
-
<gl-disclosure-dropdown
|
|
237
|
-
v-if="hasManyAgents"
|
|
238
|
-
:title="$options.i18n.CHAT_NEW_TOOLTIP"
|
|
239
|
-
:toggle-text="$options.i18n.CHAT_NEW_TOOLTIP"
|
|
240
|
-
:items="agents"
|
|
241
|
-
data-testid="chat-new-button"
|
|
242
|
-
variant="confirm"
|
|
243
|
-
category="tertiary"
|
|
244
|
-
size="small"
|
|
245
|
-
icon="duo-chat-new"
|
|
246
|
-
text-sr-only
|
|
247
|
-
:aria-label="$options.i18n.CHAT_NEW_LABEL"
|
|
248
|
-
no-caret
|
|
249
|
-
@action="startNewChat"
|
|
250
|
-
>
|
|
251
|
-
<template #list-item="{ item }">
|
|
252
|
-
<duo-chat-header-agent-item :agent="item" />
|
|
253
|
-
</template>
|
|
254
|
-
</gl-disclosure-dropdown>
|
|
255
|
-
<gl-button
|
|
256
|
-
v-else
|
|
257
|
-
v-gl-tooltip
|
|
258
|
-
:title="$options.i18n.CHAT_NEW_TOOLTIP"
|
|
259
|
-
data-testid="chat-new-button"
|
|
260
|
-
variant="confirm"
|
|
261
|
-
category="tertiary"
|
|
262
|
-
size="small"
|
|
263
|
-
icon="duo-chat-new"
|
|
264
|
-
:aria-label="$options.i18n.CHAT_NEW_LABEL"
|
|
265
|
-
@click="startNewChat"
|
|
266
|
-
/>
|
|
267
|
-
</template>
|
|
268
229
|
<gl-button
|
|
269
230
|
v-if="isMultithreaded && activeThreadId && currentView === VIEW_TYPES.LIST"
|
|
270
231
|
v-gl-tooltip
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { copyToClipboard } from '../utils';
|
|
1
2
|
import { createButton } from './buttons_utils';
|
|
2
3
|
import { createTooltip } from './tooltips_utils';
|
|
3
|
-
import { checkClipboardPermissions } from './utils';
|
|
4
4
|
|
|
5
5
|
export class CopyCodeElement extends HTMLElement {
|
|
6
6
|
constructor() {
|
|
@@ -23,25 +23,12 @@ export class CopyCodeElement extends HTMLElement {
|
|
|
23
23
|
// not present in the DOM.
|
|
24
24
|
const codeElement = this.getCodeElement();
|
|
25
25
|
const textToCopy = codeElement.innerText;
|
|
26
|
-
const hasClipboardPermission = await checkClipboardPermissions();
|
|
27
26
|
|
|
28
27
|
try {
|
|
29
|
-
codeElement
|
|
30
|
-
|
|
31
|
-
bubbles: true,
|
|
32
|
-
cancelable: true,
|
|
33
|
-
detail: {
|
|
34
|
-
code: textToCopy,
|
|
35
|
-
},
|
|
36
|
-
})
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (hasClipboardPermission) {
|
|
40
|
-
await navigator.clipboard.writeText(textToCopy);
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {
|
|
28
|
+
await copyToClipboard(textToCopy, codeElement);
|
|
29
|
+
} catch (error) {
|
|
43
30
|
// eslint-disable-next-line no-console
|
|
44
|
-
console.warn('Failed to copy
|
|
31
|
+
console.warn('Failed to copy to clipboard:', error);
|
|
45
32
|
}
|
|
46
33
|
});
|
|
47
34
|
}
|
|
@@ -15,10 +15,11 @@ import { MESSAGE_MODEL_ROLES, SELECTED_CONTEXT_ITEMS_DEFAULT_COLLAPSED } from '.
|
|
|
15
15
|
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
16
16
|
// eslint-disable-next-line no-restricted-imports
|
|
17
17
|
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
18
|
+
import { copyToClipboard, concatUntilEmpty } from '../utils';
|
|
18
19
|
import MessageFeedback from './message_feedback.vue';
|
|
19
20
|
import { CopyCodeElement } from './copy_code_element';
|
|
20
21
|
import { InsertCodeSnippetElement } from './insert_code_snippet_element';
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
import {
|
|
23
24
|
DUO_CODE_SCRIM_BOTTOM_CLASS,
|
|
24
25
|
DUO_CODE_SCRIM_OFFSET,
|
|
@@ -197,7 +198,7 @@ export default {
|
|
|
197
198
|
);
|
|
198
199
|
},
|
|
199
200
|
shouldShowCopyAction() {
|
|
200
|
-
return Boolean(
|
|
201
|
+
return Boolean(!this.isChunk && this.isAssistantMessage);
|
|
201
202
|
},
|
|
202
203
|
shouldShowFeedbackLink() {
|
|
203
204
|
return this.withFeedback && this.isNotChunkOrCancelled && this.isAssistantMessage;
|
|
@@ -302,19 +303,18 @@ export default {
|
|
|
302
303
|
}
|
|
303
304
|
},
|
|
304
305
|
async copyMessage() {
|
|
305
|
-
|
|
306
|
+
try {
|
|
307
|
+
await copyToClipboard(this.message.content, this.$el);
|
|
306
308
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
detail: {
|
|
313
|
-
message: this.message.content,
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
}
|
|
309
|
+
this.$emit('copy-message', {
|
|
310
|
+
detail: {
|
|
311
|
+
message: this.message.content,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
317
314
|
this.copied = true;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
317
|
+
console.warn('Failed to copy message:', error);
|
|
318
318
|
}
|
|
319
319
|
},
|
|
320
320
|
onOpenFilePath(e) {
|
package/src/components/chat/components/duo_chat_message_tool_approval/components/pre_block.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
|
|
3
3
|
import { translate } from '../../../../../utils/i18n';
|
|
4
|
-
import {
|
|
4
|
+
import { copyToClipboard } from '../../utils';
|
|
5
5
|
import { highlightElement } from '../services/highlight';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
@@ -41,27 +41,9 @@ export default {
|
|
|
41
41
|
async copyToClipboard() {
|
|
42
42
|
try {
|
|
43
43
|
const textToCopy = this.$refs.codeElement.textContent;
|
|
44
|
-
const hasClipboardPermission = await checkClipboardPermissions();
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
bubbles: true,
|
|
48
|
-
detail: {
|
|
49
|
-
code: textToCopy,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
// Emit custom event for VS Code and other environments
|
|
53
|
-
// This bubbles up to parent components that handle clipboard operations
|
|
54
|
-
this.$emit('copy-code-snippet', {
|
|
55
|
-
detail: {
|
|
56
|
-
code: textToCopy,
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
this.$el.dispatchEvent(event);
|
|
45
|
+
await copyToClipboard(textToCopy, this.$el);
|
|
60
46
|
|
|
61
|
-
// If we have clipboard permission, also copy directly
|
|
62
|
-
if (hasClipboardPermission) {
|
|
63
|
-
await navigator.clipboard.writeText(textToCopy);
|
|
64
|
-
}
|
|
65
47
|
this.$emit('copied', this.title);
|
|
66
48
|
setTimeout(() => {
|
|
67
49
|
this.$refs.copyToClipboardButton.$el.blur();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const concatUntilEmpty = (arr) => {
|
|
2
|
+
if (!arr) return '';
|
|
3
|
+
|
|
4
|
+
let end = arr.findIndex((el) => !el);
|
|
5
|
+
|
|
6
|
+
if (end < 0) end = arr.length;
|
|
7
|
+
|
|
8
|
+
return arr.slice(0, end).join('');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Programmatically copies a text string to the clipboard. Attempts to copy in both secure and non-secure contexts as well as VSCode.
|
|
13
|
+
*
|
|
14
|
+
* Accepts a container element. This helps ensure the text can get copied to the clipboard correctly in non-secure
|
|
15
|
+
* environments, the container should be active (such as a button in a modal) to ensure the content can be copied.
|
|
16
|
+
*
|
|
17
|
+
* @param {String} text - Text to copy
|
|
18
|
+
* @param {HTMLElement} el - element that emits event for VSCode, and contains the fallback textarea.
|
|
19
|
+
*/
|
|
20
|
+
export const copyToClipboard = (textToCopy, el) => {
|
|
21
|
+
// Fist, emit a custom event for VS Code extensions and other environments
|
|
22
|
+
// This bubbles up to parent components that handle clipboard operations
|
|
23
|
+
el.dispatchEvent(
|
|
24
|
+
new CustomEvent('copy-code-snippet', {
|
|
25
|
+
bubbles: true,
|
|
26
|
+
cancelable: true,
|
|
27
|
+
detail: {
|
|
28
|
+
code: textToCopy,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Second, for modern browsers try a simple clipboard.writeText (works on https and localhost)
|
|
34
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
35
|
+
return navigator.clipboard.writeText(textToCopy);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Third, try execCommand to copy from a dynamically created invisible textarea (for http and older browsers)
|
|
39
|
+
const textarea = document.createElement('textarea');
|
|
40
|
+
textarea.value = textToCopy;
|
|
41
|
+
textarea.style.position = 'absolute';
|
|
42
|
+
textarea.style.left = '-9999px'; // eslint-disable-line @gitlab/require-i18n-strings
|
|
43
|
+
textarea.style.top = '0';
|
|
44
|
+
textarea.setAttribute('readonly', ''); // prevent keyboard popup on mobile
|
|
45
|
+
|
|
46
|
+
// textarea must be in document to be selectable, but we add it to the button so it works in modals
|
|
47
|
+
el.appendChild(textarea);
|
|
48
|
+
|
|
49
|
+
textarea.select(); // for Safari
|
|
50
|
+
textarea.setSelectionRange(0, textarea.value.length); // for mobile devices
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const done = document.execCommand('copy');
|
|
54
|
+
el.removeChild(textarea);
|
|
55
|
+
return done ? Promise.resolve() : Promise.reject(new Error('Copy command failed'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
el.removeChild(textarea);
|
|
58
|
+
return Promise.reject(err);
|
|
59
|
+
}
|
|
60
|
+
};
|
package/translations.js
CHANGED
|
@@ -194,8 +194,6 @@ export default {
|
|
|
194
194
|
'WebDuoChat.chatEmptyStateTitle': 'I am GitLab Duo Chat, your personal AI-powered assistant.',
|
|
195
195
|
'WebDuoChat.chatHistoryTitle': 'Chat history',
|
|
196
196
|
'WebDuoChat.chatModelPlaceholder': 'GitLab Duo Chat',
|
|
197
|
-
'WebDuoChat.chatNewLabel': 'New chat',
|
|
198
|
-
'WebDuoChat.chatNewToolTip': 'New chat',
|
|
199
197
|
'WebDuoChat.chatPromptPlaceholderDefault': "Let's work through this together...",
|
|
200
198
|
'WebDuoChat.chatPromptPlaceholderWithCommands': 'Type /help to learn more',
|
|
201
199
|
'WebDuoChat.chatSubmitLabel': 'Send chat message.',
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const concatUntilEmpty = arr => {
|
|
2
|
-
if (!arr) return '';
|
|
3
|
-
let end = arr.findIndex(el => !el);
|
|
4
|
-
if (end < 0) end = arr.length;
|
|
5
|
-
return arr.slice(0, end).join('');
|
|
6
|
-
};
|
|
7
|
-
const checkClipboardPermissions = async () => {
|
|
8
|
-
try {
|
|
9
|
-
if (!navigator.clipboard || !navigator.permissions) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const permission = await navigator.permissions.query({
|
|
13
|
-
name: 'clipboard-write'
|
|
14
|
-
});
|
|
15
|
-
return permission.state === 'granted';
|
|
16
|
-
} catch (error) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export { checkClipboardPermissions, concatUntilEmpty };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export const concatUntilEmpty = (arr) => {
|
|
2
|
-
if (!arr) return '';
|
|
3
|
-
|
|
4
|
-
let end = arr.findIndex((el) => !el);
|
|
5
|
-
|
|
6
|
-
if (end < 0) end = arr.length;
|
|
7
|
-
|
|
8
|
-
return arr.slice(0, end).join('');
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const checkClipboardPermissions = async () => {
|
|
12
|
-
try {
|
|
13
|
-
if (!navigator.clipboard || !navigator.permissions) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const permission = await navigator.permissions.query({ name: 'clipboard-write' });
|
|
18
|
-
return permission.state === 'granted';
|
|
19
|
-
} catch (error) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
};
|