@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.
Files changed (25) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/components/agentic_chat/agentic_duo_chat.js +37 -18
  3. package/dist/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow.js +51 -3
  4. package/dist/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_modal/agentic_tool_approval_modal.js +64 -14
  5. package/dist/components/chat/components/duo_chat_header/duo_chat_header.js +4 -4
  6. package/dist/components/chat/duo_chat.js +54 -35
  7. package/dist/components.css +1 -1
  8. package/dist/components.css.map +1 -1
  9. package/dist/index.js +0 -2
  10. package/dist/tailwind.css +1 -1
  11. package/dist/tailwind.css.map +1 -1
  12. package/package.json +1 -2
  13. package/src/components/agentic_chat/agentic_duo_chat.vue +244 -210
  14. package/src/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_flow.vue +57 -2
  15. package/src/components/agentic_chat/components/agentic_tool_approval_flow/agentic_tool_approval_modal/agentic_tool_approval_modal.vue +105 -16
  16. package/src/components/chat/components/duo_chat_header/duo_chat_header.vue +22 -24
  17. package/src/components/chat/duo_chat.scss +2 -1
  18. package/src/components/chat/duo_chat.vue +238 -214
  19. package/src/index.js +0 -2
  20. package/translations.js +6 -5
  21. package/dist/components/ui/duo_layout/duo_layout.js +0 -100
  22. package/dist/components/ui/side_rail/side_rail.js +0 -67
  23. package/src/components/ui/duo_layout/duo_layout.md +0 -0
  24. package/src/components/ui/duo_layout/duo_layout.vue +0 -95
  25. 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 may be inaccurate. Verify before use.'
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
- <div
759
- id="chat-component"
760
- class="markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
761
- role="complementary"
762
- data-testid="chat-component"
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
- <duo-chat-header
765
- v-if="showHeader"
766
- ref="header"
767
- :active-thread-id="activeThreadId"
768
- :title="isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title"
769
- :subtitle="activeThreadTitleForView"
770
- :error="error"
771
- :is-multithreaded="isMultithreaded"
772
- :current-view="currentView"
773
- :should-render-resizable="shouldRenderResizable"
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-threads
792
- v-if="shouldShowThreadList"
793
- :threads="threadList"
794
- :preferred-locale="preferredLocale"
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
- <duo-chat-conversation
808
- v-for="(conversation, index) in conversations"
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
- <duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
842
- <div key="anchor" ref="anchor" class="scroll-anchor"></div>
843
- </transition-group>
844
- </div>
845
- <footer
846
- v-if="isChatAvailable && !shouldShowThreadList"
847
- data-testid="chat-footer"
848
- class="duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3"
849
- :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
850
- >
851
- <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
852
- <div class="gl-relative gl-max-w-full">
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
- <slot
857
- name="context-items-menu"
858
- :is-open="contextItemsMenuIsOpen"
859
- :on-close="closeContextItemsMenuOpen"
860
- :set-ref="setContextItemsMenuRef"
861
- :focus-prompt="focusChatInput"
862
- ></slot>
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
- <div :data-value="prompt" class="gl-h-[40px] gl-grow">
875
- <gl-card
876
- v-if="shouldShowSlashCommands"
877
- ref="commands"
878
- class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
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-dropdown-item
882
- v-for="(command, index) in filteredSlashCommands"
883
- :key="command.name"
884
- :class="{ 'active-command': index === activeCommandIndex }"
885
- @mouseenter.native="activeCommandIndex = index"
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
- <span class="gl-flex gl-justify-between">
889
- <span class="gl-block">{{ command.name }}</span>
890
- <small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
891
- command.description
892
- }}</small>
893
- </span>
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
- {{ remainingCharacterCountMessage(count) }}
932
- </span>
933
- </template>
934
- <template #character-count-over-limit-text="{ count }">
935
- <span class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3">{{
936
- overLimitCharacterCountMessage(count)
937
- }}</span>
938
- </template>
939
- </gl-form-textarea>
940
- </div>
941
- <div class="gl-flex gl-justify-end gl-px-3 gl-pb-3">
942
- <gl-button
943
- v-if="canSubmit"
944
- icon="paper-airplane"
945
- category="primary"
946
- variant="confirm"
947
- class="gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full"
948
- type="submit"
949
- :disabled="isPromptEmpty || !hasValidPrompt"
950
- data-testid="chat-prompt-submit-button"
951
- :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
952
- />
953
- <gl-button
954
- v-else
955
- icon="stop"
956
- category="primary"
957
- variant="default"
958
- class="gl-bottom-2 gl-right-2 !gl-rounded-full"
959
- data-testid="chat-prompt-cancel-button"
960
- :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
961
- @click="cancelPrompt"
962
- />
963
- </div>
964
- </div>
965
- </gl-form>
966
- <slot name="footer-controls"></slot>
967
- <p
968
- class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary"
969
- :class="{ 'gl-mt-6 sm:gl-mt-3 sm:gl-max-w-1/2': prompt.length >= maxPromptLengthWarning }"
970
- >
971
- {{ $options.i18n.CHAT_DISCLAMER }}
972
- </p>
973
- </footer>
974
- </div>
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"