@gitlab/duo-ui 10.22.1 → 10.23.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 +22 -0
- package/dist/components/agentic_chat/agentic_duo_chat.js +37 -18
- package/dist/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow.js +51 -3
- package/dist/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_modal/agentic_tool_approval_modal.js +64 -14
- package/dist/components/chat/components/duo_chat_header/duo_chat_header.js +4 -4
- package/dist/components/chat/duo_chat.js +54 -35
- package/dist/components.css +1 -1
- package/dist/components.css.map +1 -1
- package/dist/index.js +0 -2
- package/dist/tailwind.css +1 -1
- package/dist/tailwind.css.map +1 -1
- package/package.json +1 -2
- package/src/components/agentic_chat/agentic_duo_chat.vue +244 -210
- package/src/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow.vue +57 -2
- package/src/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_modal/agentic_tool_approval_modal.vue +105 -16
- package/src/components/chat/components/duo_chat_header/duo_chat_header.vue +22 -24
- package/src/components/chat/duo_chat.scss +2 -1
- package/src/components/chat/duo_chat.vue +238 -214
- package/src/index.js +0 -2
- package/translations.js +6 -5
- package/dist/components/ui/duo_layout/duo_layout.js +0 -100
- package/dist/components/ui/side_rail/side_rail.js +0 -67
- package/src/components/ui/duo_layout/duo_layout.md +0 -0
- package/src/components/ui/duo_layout/duo_layout.vue +0 -95
- package/src/components/ui/side_rail/side_rail.vue +0 -56
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import throttle from 'lodash/throttle';
|
|
3
|
+
import VueResizable from 'vue-resizable';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
GlButton,
|
|
6
7
|
GlDropdownItem,
|
|
7
8
|
GlCard,
|
|
9
|
+
GlAlert,
|
|
10
|
+
GlFormInputGroup,
|
|
8
11
|
GlFormTextarea,
|
|
9
12
|
GlForm,
|
|
13
|
+
GlExperimentBadge,
|
|
10
14
|
GlSafeHtmlDirective as SafeHtml,
|
|
11
15
|
} from '@gitlab/ui';
|
|
12
16
|
|
|
@@ -34,7 +38,7 @@ export const i18n = {
|
|
|
34
38
|
CHAT_HISTORY_TITLE: translate('AgenticDuoChat.chatHistoryTitle', 'Chat history'),
|
|
35
39
|
CHAT_DISCLAMER: translate(
|
|
36
40
|
'AgenticDuoChat.chatDisclamer',
|
|
37
|
-
'Responses
|
|
41
|
+
'Chat can autonomously change code. Responses and changes can be inaccurate. Review carefully.'
|
|
38
42
|
),
|
|
39
43
|
CHAT_EMPTY_STATE_TITLE: translate(
|
|
40
44
|
'AgenticDuoChat.chatEmptyStateTitle',
|
|
@@ -42,10 +46,6 @@ export const i18n = {
|
|
|
42
46
|
),
|
|
43
47
|
CHAT_PROMPT_PLACEHOLDER_DEFAULT: translate(
|
|
44
48
|
'AgenticDuoChat.chatPromptPlaceholderDefault',
|
|
45
|
-
"Let's work through this together..."
|
|
46
|
-
),
|
|
47
|
-
CHAT_MODEL_PLACEHOLDER: translate(
|
|
48
|
-
'AgenticDuoChat.chatModelPlaceholder',
|
|
49
49
|
'GitLab Duo Agentic Chat'
|
|
50
50
|
),
|
|
51
51
|
CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS: translate(
|
|
@@ -102,8 +102,11 @@ export default {
|
|
|
102
102
|
name: 'DuoChat',
|
|
103
103
|
components: {
|
|
104
104
|
GlButton,
|
|
105
|
+
GlAlert,
|
|
106
|
+
GlFormInputGroup,
|
|
105
107
|
GlFormTextarea,
|
|
106
108
|
GlForm,
|
|
109
|
+
GlExperimentBadge,
|
|
107
110
|
DuoChatLoader,
|
|
108
111
|
DuoChatPredefinedPrompts,
|
|
109
112
|
DuoChatConversation,
|
|
@@ -111,6 +114,7 @@ export default {
|
|
|
111
114
|
DuoChatThreads,
|
|
112
115
|
GlCard,
|
|
113
116
|
GlDropdownItem,
|
|
117
|
+
VueResizable,
|
|
114
118
|
},
|
|
115
119
|
directives: {
|
|
116
120
|
SafeHtml,
|
|
@@ -339,6 +343,7 @@ export default {
|
|
|
339
343
|
},
|
|
340
344
|
data() {
|
|
341
345
|
return {
|
|
346
|
+
isHidden: false,
|
|
342
347
|
prompt: '',
|
|
343
348
|
scrolledToBottom: true,
|
|
344
349
|
activeCommandIndex: 0,
|
|
@@ -456,6 +461,7 @@ export default {
|
|
|
456
461
|
if (!loading && !this.isStreaming) {
|
|
457
462
|
this.canSubmit = true; // Re-enable submit button when loading stops
|
|
458
463
|
}
|
|
464
|
+
this.isHidden = false;
|
|
459
465
|
},
|
|
460
466
|
isStreaming(streaming) {
|
|
461
467
|
if (!streaming && !this.isLoading) {
|
|
@@ -496,10 +502,14 @@ export default {
|
|
|
496
502
|
this.focusChatInput();
|
|
497
503
|
});
|
|
498
504
|
},
|
|
505
|
+
updateSize(e) {
|
|
506
|
+
this.$emit('chat-resize', e);
|
|
507
|
+
},
|
|
499
508
|
compositionEnd() {
|
|
500
509
|
this.compositionJustEnded = true;
|
|
501
510
|
},
|
|
502
511
|
hideChat() {
|
|
512
|
+
this.isHidden = true;
|
|
503
513
|
/**
|
|
504
514
|
* Emitted when clicking the cross in the title and the chat gets closed.
|
|
505
515
|
*/
|
|
@@ -755,221 +765,245 @@ export default {
|
|
|
755
765
|
};
|
|
756
766
|
</script>
|
|
757
767
|
<template>
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
768
|
+
<component
|
|
769
|
+
:is="shouldRenderResizable ? 'vue-resizable' : 'div'"
|
|
770
|
+
:width="shouldRenderResizable ? dimensions.width : null"
|
|
771
|
+
:height="shouldRenderResizable ? dimensions.height : null"
|
|
772
|
+
:max-width="shouldRenderResizable ? dimensions.maxWidth : null"
|
|
773
|
+
:max-height="shouldRenderResizable ? dimensions.maxHeight : null"
|
|
774
|
+
:min-width="shouldRenderResizable ? dimensions.minWidth : null"
|
|
775
|
+
:left="shouldRenderResizable ? dimensions.left : null"
|
|
776
|
+
:top="shouldRenderResizable ? dimensions.top : null"
|
|
777
|
+
:fit-parent="true"
|
|
778
|
+
:min-height="shouldRenderResizable ? dimensions.minHeight : null"
|
|
779
|
+
:class="{
|
|
780
|
+
'duo-chat-resizable': shouldRenderResizable,
|
|
781
|
+
'non-resizable-wrapper': !shouldRenderResizable,
|
|
782
|
+
}"
|
|
783
|
+
:active="shouldRenderResizable ? ['l', 't', 'lt'] : null"
|
|
784
|
+
@resize:end="updateSize"
|
|
763
785
|
>
|
|
764
|
-
<
|
|
765
|
-
v-if="
|
|
766
|
-
|
|
767
|
-
:
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
:badge-type="isMultithreaded ? null : badgeType"
|
|
775
|
-
:session-id="sessionId"
|
|
776
|
-
:agents="agents"
|
|
777
|
-
@go-back="onGoBack"
|
|
778
|
-
@new-chat="onNewChat"
|
|
779
|
-
@close="hideChat"
|
|
780
|
-
>
|
|
781
|
-
<template #subheader>
|
|
782
|
-
<slot name="subheader"></slot>
|
|
783
|
-
</template>
|
|
784
|
-
</duo-chat-header>
|
|
785
|
-
|
|
786
|
-
<div
|
|
787
|
-
class="gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit"
|
|
788
|
-
data-testid="chat-history"
|
|
789
|
-
@scroll="handleScrollingThrottled"
|
|
786
|
+
<aside
|
|
787
|
+
v-if="!isHidden"
|
|
788
|
+
id="chat-component"
|
|
789
|
+
:class="{
|
|
790
|
+
'resizable-content': shouldRenderResizable,
|
|
791
|
+
'duo-chat-drawer': !shouldRenderResizable,
|
|
792
|
+
}"
|
|
793
|
+
class="markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
|
|
794
|
+
role="complementary"
|
|
795
|
+
data-testid="chat-component"
|
|
790
796
|
>
|
|
791
|
-
<duo-chat-
|
|
792
|
-
v-if="
|
|
793
|
-
|
|
794
|
-
:
|
|
797
|
+
<duo-chat-header
|
|
798
|
+
v-if="showHeader"
|
|
799
|
+
ref="header"
|
|
800
|
+
:active-thread-id="activeThreadId"
|
|
801
|
+
:title="
|
|
802
|
+
isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title
|
|
803
|
+
"
|
|
804
|
+
:subtitle="activeThreadTitleForView"
|
|
805
|
+
:error="error"
|
|
806
|
+
:is-multithreaded="isMultithreaded"
|
|
807
|
+
:current-view="currentView"
|
|
808
|
+
:should-render-resizable="shouldRenderResizable"
|
|
809
|
+
:badge-type="isMultithreaded ? null : badgeType"
|
|
810
|
+
:session-id="sessionId"
|
|
811
|
+
:agents="agents"
|
|
812
|
+
@go-back="onGoBack"
|
|
795
813
|
@new-chat="onNewChat"
|
|
796
|
-
@select-thread="onSelectThread"
|
|
797
|
-
@delete-thread="onDeleteThread"
|
|
798
814
|
@close="hideChat"
|
|
799
|
-
/>
|
|
800
|
-
<transition-group
|
|
801
|
-
v-else
|
|
802
|
-
mode="out-in"
|
|
803
|
-
tag="section"
|
|
804
|
-
name="message"
|
|
805
|
-
class="duo-chat-history gl-mt-auto gl-p-5"
|
|
806
815
|
>
|
|
807
|
-
<
|
|
808
|
-
|
|
809
|
-
:key="`conversation-${index}`"
|
|
810
|
-
:enable-code-insertion="enableCodeInsertion"
|
|
811
|
-
:messages="conversation"
|
|
812
|
-
:show-delimiter="index > 0"
|
|
813
|
-
:with-feedback="withFeedback"
|
|
814
|
-
:is-tool-approval-processing="isToolApprovalProcessing"
|
|
815
|
-
:working-directory="workingDirectory"
|
|
816
|
-
@track-feedback="onTrackFeedback"
|
|
817
|
-
@insert-code-snippet="onInsertCodeSnippet"
|
|
818
|
-
@copy-code-snippet="onCopyCodeSnippet"
|
|
819
|
-
@copy-message="onCopyMessage"
|
|
820
|
-
@get-context-item-content="onGetContextItemContent"
|
|
821
|
-
@approve-tool="onApproveToolCall"
|
|
822
|
-
@deny-tool="onDenyToolCall"
|
|
823
|
-
@open-file-path="onOpenFilePath"
|
|
824
|
-
/>
|
|
825
|
-
<template v-if="!hasMessages && !isLoading">
|
|
826
|
-
<div
|
|
827
|
-
key="empty-state-message"
|
|
828
|
-
class="duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-gray-900 gl-break-anywhere"
|
|
829
|
-
data-testid="gl-duo-chat-empty-state"
|
|
830
|
-
>
|
|
831
|
-
<p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
|
|
832
|
-
{{ emptyStateTitle }}
|
|
833
|
-
</p>
|
|
834
|
-
<duo-chat-predefined-prompts
|
|
835
|
-
key="predefined-prompts"
|
|
836
|
-
:prompts="predefinedPrompts"
|
|
837
|
-
@click="sendPredefinedPrompt"
|
|
838
|
-
/>
|
|
839
|
-
</div>
|
|
816
|
+
<template #subheader>
|
|
817
|
+
<slot name="subheader"></slot>
|
|
840
818
|
</template>
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
819
|
+
</duo-chat-header>
|
|
820
|
+
|
|
821
|
+
<div
|
|
822
|
+
class="gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit"
|
|
823
|
+
data-testid="chat-history"
|
|
824
|
+
@scroll="handleScrollingThrottled"
|
|
825
|
+
>
|
|
826
|
+
<duo-chat-threads
|
|
827
|
+
v-if="shouldShowThreadList"
|
|
828
|
+
:threads="threadList"
|
|
829
|
+
:preferred-locale="preferredLocale"
|
|
830
|
+
@new-chat="onNewChat"
|
|
831
|
+
@select-thread="onSelectThread"
|
|
832
|
+
@delete-thread="onDeleteThread"
|
|
833
|
+
@close="hideChat"
|
|
834
|
+
/>
|
|
835
|
+
<transition-group
|
|
836
|
+
v-else
|
|
837
|
+
mode="out-in"
|
|
838
|
+
tag="section"
|
|
839
|
+
name="message"
|
|
840
|
+
class="duo-chat-history gl-mt-auto gl-p-5"
|
|
841
|
+
>
|
|
842
|
+
<duo-chat-conversation
|
|
843
|
+
v-for="(conversation, index) in conversations"
|
|
844
|
+
:key="`conversation-${index}`"
|
|
845
|
+
:enable-code-insertion="enableCodeInsertion"
|
|
846
|
+
:messages="conversation"
|
|
847
|
+
:show-delimiter="index > 0"
|
|
848
|
+
:with-feedback="withFeedback"
|
|
849
|
+
:is-tool-approval-processing="isToolApprovalProcessing"
|
|
850
|
+
:working-directory="workingDirectory"
|
|
851
|
+
@track-feedback="onTrackFeedback"
|
|
852
|
+
@insert-code-snippet="onInsertCodeSnippet"
|
|
853
|
+
@copy-code-snippet="onCopyCodeSnippet"
|
|
854
|
+
@copy-message="onCopyMessage"
|
|
855
|
+
@get-context-item-content="onGetContextItemContent"
|
|
856
|
+
@approve-tool="onApproveToolCall"
|
|
857
|
+
@deny-tool="onDenyToolCall"
|
|
858
|
+
@open-file-path="onOpenFilePath"
|
|
859
|
+
/>
|
|
860
|
+
<template v-if="!hasMessages && !isLoading">
|
|
861
|
+
<div
|
|
862
|
+
key="empty-state-message"
|
|
863
|
+
class="duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-gray-900 gl-break-anywhere"
|
|
864
|
+
data-testid="gl-duo-chat-empty-state"
|
|
865
|
+
>
|
|
866
|
+
<p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
|
|
867
|
+
{{ emptyStateTitle }}
|
|
868
|
+
</p>
|
|
869
|
+
<duo-chat-predefined-prompts
|
|
870
|
+
key="predefined-prompts"
|
|
871
|
+
:prompts="predefinedPrompts"
|
|
872
|
+
@click="sendPredefinedPrompt"
|
|
873
|
+
/>
|
|
874
|
+
</div>
|
|
875
|
+
</template>
|
|
876
|
+
<duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
|
|
877
|
+
<div key="anchor" ref="anchor" class="scroll-anchor"></div>
|
|
878
|
+
</transition-group>
|
|
879
|
+
</div>
|
|
880
|
+
<footer
|
|
881
|
+
v-if="isChatAvailable && !shouldShowThreadList"
|
|
882
|
+
data-testid="chat-footer"
|
|
883
|
+
class="duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3"
|
|
884
|
+
:class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
|
|
885
|
+
>
|
|
886
|
+
<gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
|
|
887
|
+
<div class="gl-relative gl-max-w-full">
|
|
888
|
+
<!--
|
|
854
889
|
@slot For integrating `<gl-context-items-menu>` component if pinned-context should be available. The following scopedSlot properties are provided: `isOpen`, `onClose`, `setRef`, `focusPrompt`, which should be passed to the `<gl-context-items-menu>` component when rendering, e.g. `<template #context-items-menu="{ isOpen, onClose, setRef, focusPrompt }">` `<duo-chat-context-item-menu :ref="setRef" :open="isOpen" @close="onClose" @focus-prompt="focusPrompt" ...`
|
|
855
890
|
-->
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
</div>
|
|
864
|
-
|
|
865
|
-
<div
|
|
866
|
-
class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-flex-col gl-rounded-bl-[12px] gl-rounded-br-[18px] gl-rounded-tl-[12px] gl-rounded-tr-[12px] gl-align-top"
|
|
867
|
-
>
|
|
868
|
-
<div
|
|
869
|
-
class="gl-flex gl-justify-between gl-border-0 gl-border-b-1 gl-border-solid gl-border-[#DCDCDE] gl-px-4 gl-py-4"
|
|
870
|
-
>
|
|
871
|
-
<div>{{ $options.i18n.CHAT_MODEL_PLACEHOLDER }}</div>
|
|
872
|
-
<div><slot name="agentic-switch"></slot></div>
|
|
891
|
+
<slot
|
|
892
|
+
name="context-items-menu"
|
|
893
|
+
:is-open="contextItemsMenuIsOpen"
|
|
894
|
+
:on-close="closeContextItemsMenuOpen"
|
|
895
|
+
:set-ref="setContextItemsMenuRef"
|
|
896
|
+
:focus-prompt="focusChatInput"
|
|
897
|
+
></slot>
|
|
873
898
|
</div>
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
body-class="!gl-p-2"
|
|
899
|
+
|
|
900
|
+
<gl-form-input-group>
|
|
901
|
+
<div
|
|
902
|
+
class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top"
|
|
903
|
+
:data-value="prompt"
|
|
880
904
|
>
|
|
881
|
-
<gl-
|
|
882
|
-
v-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
@click="selectSlashCommand(index)"
|
|
905
|
+
<gl-card
|
|
906
|
+
v-if="shouldShowSlashCommands"
|
|
907
|
+
ref="commands"
|
|
908
|
+
class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
|
|
909
|
+
body-class="!gl-p-2"
|
|
887
910
|
>
|
|
888
|
-
<
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
</gl-dropdown-item>
|
|
895
|
-
</gl-card>
|
|
896
|
-
|
|
897
|
-
<gl-form-textarea
|
|
898
|
-
ref="prompt"
|
|
899
|
-
v-model="prompt"
|
|
900
|
-
:disabled="!canSubmit"
|
|
901
|
-
data-testid="chat-prompt-input"
|
|
902
|
-
:placeholder="inputPlaceholder"
|
|
903
|
-
:character-count-limit="maxPromptLength"
|
|
904
|
-
:textarea-classes="[
|
|
905
|
-
'!gl-h-full',
|
|
906
|
-
'!gl-bg-transparent',
|
|
907
|
-
'!gl-py-4',
|
|
908
|
-
'!gl-shadow-none',
|
|
909
|
-
'form-control',
|
|
910
|
-
'gl-form-input',
|
|
911
|
-
'gl-form-textarea',
|
|
912
|
-
{ 'gl-truncate': !prompt },
|
|
913
|
-
]"
|
|
914
|
-
aria-label="Chat prompt input"
|
|
915
|
-
autofocus
|
|
916
|
-
@keydown.enter.exact.native.prevent
|
|
917
|
-
@keydown.ctrl.z.exact="handleUndo"
|
|
918
|
-
@keydown.meta.z.exact="handleUndo"
|
|
919
|
-
@keydown.ctrl.shift.z.exact="handleRedo"
|
|
920
|
-
@keydown.meta.shift.z.exact="handleRedo"
|
|
921
|
-
@keydown.ctrl.y.exact="handleRedo"
|
|
922
|
-
@keydown.meta.y.exact="handleRedo"
|
|
923
|
-
@keyup.native="onInputKeyup"
|
|
924
|
-
@compositionend="compositionEnd"
|
|
925
|
-
>
|
|
926
|
-
<template #remaining-character-count-text="{ count }">
|
|
927
|
-
<span
|
|
928
|
-
v-if="count <= promptLengthWarningCount"
|
|
929
|
-
class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"
|
|
911
|
+
<gl-dropdown-item
|
|
912
|
+
v-for="(command, index) in filteredSlashCommands"
|
|
913
|
+
:key="command.name"
|
|
914
|
+
:class="{ 'active-command': index === activeCommandIndex }"
|
|
915
|
+
@mouseenter.native="activeCommandIndex = index"
|
|
916
|
+
@click="selectSlashCommand(index)"
|
|
930
917
|
>
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
</
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
918
|
+
<span class="gl-flex gl-justify-between">
|
|
919
|
+
<span class="gl-block">{{ command.name }}</span>
|
|
920
|
+
<small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
|
|
921
|
+
command.description
|
|
922
|
+
}}</small>
|
|
923
|
+
</span>
|
|
924
|
+
</gl-dropdown-item>
|
|
925
|
+
</gl-card>
|
|
926
|
+
|
|
927
|
+
<gl-form-textarea
|
|
928
|
+
ref="prompt"
|
|
929
|
+
v-model="prompt"
|
|
930
|
+
:disabled="!canSubmit"
|
|
931
|
+
data-testid="chat-prompt-input"
|
|
932
|
+
:placeholder="inputPlaceholder"
|
|
933
|
+
:character-count-limit="maxPromptLength"
|
|
934
|
+
:textarea-classes="[
|
|
935
|
+
'gl-absolute',
|
|
936
|
+
'!gl-h-full',
|
|
937
|
+
'gl-rounded-br-none',
|
|
938
|
+
'gl-rounded-tr-none',
|
|
939
|
+
'!gl-bg-transparent',
|
|
940
|
+
'!gl-py-4',
|
|
941
|
+
'!gl-shadow-none',
|
|
942
|
+
'form-control',
|
|
943
|
+
'gl-form-input',
|
|
944
|
+
'gl-form-textarea',
|
|
945
|
+
{ 'gl-truncate': !prompt },
|
|
946
|
+
]"
|
|
947
|
+
aria-label="Chat prompt input"
|
|
948
|
+
autofocus
|
|
949
|
+
@keydown.enter.exact.native.prevent
|
|
950
|
+
@keydown.ctrl.z.exact="handleUndo"
|
|
951
|
+
@keydown.meta.z.exact="handleUndo"
|
|
952
|
+
@keydown.ctrl.shift.z.exact="handleRedo"
|
|
953
|
+
@keydown.meta.shift.z.exact="handleRedo"
|
|
954
|
+
@keydown.ctrl.y.exact="handleRedo"
|
|
955
|
+
@keydown.meta.y.exact="handleRedo"
|
|
956
|
+
@keyup.native="onInputKeyup"
|
|
957
|
+
@compositionend="compositionEnd"
|
|
958
|
+
>
|
|
959
|
+
<template #remaining-character-count-text="{ count }">
|
|
960
|
+
<span
|
|
961
|
+
v-if="count <= promptLengthWarningCount"
|
|
962
|
+
class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"
|
|
963
|
+
>
|
|
964
|
+
{{ remainingCharacterCountMessage(count) }}
|
|
965
|
+
</span>
|
|
966
|
+
</template>
|
|
967
|
+
<template #character-count-over-limit-text="{ count }">
|
|
968
|
+
<span class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3">{{
|
|
969
|
+
overLimitCharacterCountMessage(count)
|
|
970
|
+
}}</span>
|
|
971
|
+
</template>
|
|
972
|
+
</gl-form-textarea>
|
|
973
|
+
</div>
|
|
974
|
+
<template #append>
|
|
975
|
+
<gl-button
|
|
976
|
+
v-if="canSubmit"
|
|
977
|
+
icon="paper-airplane"
|
|
978
|
+
category="primary"
|
|
979
|
+
variant="confirm"
|
|
980
|
+
class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
|
|
981
|
+
type="submit"
|
|
982
|
+
:disabled="isPromptEmpty || !hasValidPrompt"
|
|
983
|
+
data-testid="chat-prompt-submit-button"
|
|
984
|
+
:aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
|
|
985
|
+
/>
|
|
986
|
+
<gl-button
|
|
987
|
+
v-else
|
|
988
|
+
icon="stop"
|
|
989
|
+
category="primary"
|
|
990
|
+
variant="default"
|
|
991
|
+
class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
|
|
992
|
+
data-testid="chat-prompt-cancel-button"
|
|
993
|
+
:aria-label="$options.i18n.CHAT_CANCEL_LABEL"
|
|
994
|
+
@click="cancelPrompt"
|
|
995
|
+
/>
|
|
996
|
+
</template>
|
|
997
|
+
</gl-form-input-group>
|
|
998
|
+
</gl-form>
|
|
999
|
+
<slot name="footer-controls"></slot>
|
|
1000
|
+
<p
|
|
1001
|
+
class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary"
|
|
1002
|
+
:class="{ 'gl-mt-6 sm:gl-mt-3 sm:gl-max-w-1/2': prompt.length >= maxPromptLengthWarning }"
|
|
1003
|
+
>
|
|
1004
|
+
{{ $options.i18n.CHAT_DISCLAMER }}
|
|
1005
|
+
</p>
|
|
1006
|
+
</footer>
|
|
1007
|
+
</aside>
|
|
1008
|
+
</component>
|
|
975
1009
|
</template>
|
|
@@ -15,6 +15,14 @@ const MODAL_TYPES = {
|
|
|
15
15
|
REJECTION: 'rejection',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const DEFAULT_APPROVAL_OPTIONS = [
|
|
19
|
+
{
|
|
20
|
+
type: 'approve-tool-once',
|
|
21
|
+
text: 'Approve',
|
|
22
|
+
primary: true,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
18
26
|
export default {
|
|
19
27
|
name: 'AgenticToolApprovalFlow',
|
|
20
28
|
components: {
|
|
@@ -50,6 +58,50 @@ export default {
|
|
|
50
58
|
required: false,
|
|
51
59
|
default: i18n.DEFAULT_DESCRIPTION,
|
|
52
60
|
},
|
|
61
|
+
/**
|
|
62
|
+
* Configuration for approval options
|
|
63
|
+
* Must include at least one option with type 'approve-tool-once' for legacy compatibility
|
|
64
|
+
* @property {string} type - Event payload identifier (must include 'approve-tool-once')
|
|
65
|
+
* @property {string} text - Display text for button/dropdown
|
|
66
|
+
* @property {boolean} primary - Mark as primary action (exactly one required)
|
|
67
|
+
* @property {boolean} disabled - Disable this option (passed through to GitLab UI)
|
|
68
|
+
*/
|
|
69
|
+
approvalOptions: {
|
|
70
|
+
type: Array,
|
|
71
|
+
required: false,
|
|
72
|
+
default: () => DEFAULT_APPROVAL_OPTIONS,
|
|
73
|
+
validator: (options) => {
|
|
74
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const primaryCount = options.filter((option) => option.primary === true).length;
|
|
79
|
+
if (primaryCount !== 1) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const types = options.map((option) => option.type);
|
|
84
|
+
if (new Set(types).size !== types.length) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Require 'approve-tool-once' type for legacy compatibility
|
|
89
|
+
const hasApproveOnce = types.includes('approve-tool-once');
|
|
90
|
+
if (!hasApproveOnce) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return options.every((option) => {
|
|
95
|
+
if (!option.type || !option.text) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (option.primary === true && option.disabled === true) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
},
|
|
53
105
|
},
|
|
54
106
|
data() {
|
|
55
107
|
return {
|
|
@@ -72,11 +124,13 @@ export default {
|
|
|
72
124
|
},
|
|
73
125
|
},
|
|
74
126
|
methods: {
|
|
75
|
-
handleApprove() {
|
|
127
|
+
handleApprove(approvalPayload) {
|
|
76
128
|
/**
|
|
77
129
|
* Emitted when the user approves the tool execution
|
|
130
|
+
* @param {Object} approvalPayload - Contains the approval type and any additional data
|
|
131
|
+
* @param {string} approvalPayload.type - The type of approval ('once', 'session', 'all')
|
|
78
132
|
*/
|
|
79
|
-
this.$emit('approve');
|
|
133
|
+
this.$emit('approve', approvalPayload);
|
|
80
134
|
},
|
|
81
135
|
handleInitialDeny() {
|
|
82
136
|
this.currentModal = MODAL_TYPES.REJECTION;
|
|
@@ -105,6 +159,7 @@ export default {
|
|
|
105
159
|
:visible="showApprovalModal"
|
|
106
160
|
:tool-details="toolDetails"
|
|
107
161
|
:description="description"
|
|
162
|
+
:approval-options="approvalOptions"
|
|
108
163
|
@approve="handleApprove"
|
|
109
164
|
@deny="handleInitialDeny"
|
|
110
165
|
@deny-force="handleForceDeny"
|