@adminforth/agent 2.9.1 → 3.0.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.
Files changed (34) hide show
  1. package/README.md +4 -0
  2. package/dist/application/runTurnUseCase.d.ts +2 -1
  3. package/dist/application/runTurnUseCase.js +3 -3
  4. package/dist/custom/ChatFooter.vue +91 -104
  5. package/dist/custom/ChatHeader.vue +63 -62
  6. package/dist/custom/ChatSurface.vue +11 -18
  7. package/dist/custom/EditActionButtons.vue +12 -8
  8. package/dist/custom/SessionsHistory.vue +90 -28
  9. package/dist/custom/SteerQueue.vue +15 -9
  10. package/dist/custom/composables/agentStore/constants.ts +1 -1
  11. package/dist/custom/composables/agentStore/useAgentChat.ts +4 -3
  12. package/dist/custom/composables/agentStore/useAgentPlaceholder.ts +1 -0
  13. package/dist/custom/composables/agentStore/useAgentSessions.ts +23 -2
  14. package/dist/custom/composables/useAgentStore.ts +8 -29
  15. package/dist/custom/conversation_area/ConversationArea.vue +48 -16
  16. package/dist/custom/conversation_area/MessageRenderer.vue +18 -13
  17. package/dist/custom/conversation_area/ProcessingTimeline.vue +83 -123
  18. package/dist/custom/conversation_area/ReasoningRenderer.vue +13 -16
  19. package/dist/custom/conversation_area/SystemMessageRenderer.vue +4 -4
  20. package/dist/custom/conversation_area/TextRenderer.vue +67 -28
  21. package/dist/custom/conversation_area/ToolApprovalRenderer.vue +89 -56
  22. package/dist/custom/conversation_area/ToolRenderer.vue +44 -82
  23. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +22 -61
  24. package/dist/custom/speech_recognition_frontend/AudioLines.vue +1 -2
  25. package/dist/custom/speech_recognition_frontend/MicrophoneButon.vue +36 -32
  26. package/dist/custom/types.ts +16 -8
  27. package/dist/domain/agentEvents.d.ts +3 -2
  28. package/dist/tools/approvalFormatter.d.ts +19 -5
  29. package/dist/tools/approvalFormatter.js +88 -40
  30. package/dist/transport/http/context.d.ts +2 -1
  31. package/dist/transport/http/sessionEndpoints.js +34 -0
  32. package/package.json +3 -3
  33. package/dist/custom/SteerDebugPanel.vue +0 -45
  34. package/dist/custom/conversation_area/ToolsGroup.vue +0 -59
@@ -1,79 +1,112 @@
1
1
  <template>
2
- <div class="mx-4 my-3 max-w-[min(34rem,calc(100%-2rem))] z-20 rounded-lg border border-lightPrimary/30 bg-lightNavbar p-4 text-lightListTableHeadingText shadow-sm dark:border-darkPrimary/40 dark:bg-darkNavbar dark:text-darkListTableHeadingText">
3
- <div class="flex items-start gap-3">
4
- <div class="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/15 dark:text-darkPrimary">
5
- <IconExclamationCircleOutline class="h-5 w-5" />
6
- </div>
2
+ <div
3
+ class="af-agent-approval mx-4 my-2 max-w-[min(34rem,calc(100%-2rem))] z-20 rounded-lg border p-3.5"
4
+ :class="isAwaitingDecision
5
+ ? 'border-lightWarningBorder bg-lightWarningSoft dark:border-darkWarningBorder dark:bg-darkWarningSoft'
6
+ : 'border-lightCardBorder bg-lightCardBackground dark:border-darkCardBorder dark:bg-darkCardBackground'"
7
+ >
8
+ <div class="flex items-start gap-2.5">
9
+ <IconShieldAlert
10
+ class="mt-0.5 h-4 w-4 shrink-0"
11
+ :class="isAwaitingDecision ? 'text-lightWarning dark:text-darkWarning' : 'text-lightInputIconColor dark:text-darkInputIconColor'"
12
+ />
7
13
  <div class="min-w-0 flex-1">
8
- <h3 class="text-sm font-semibold leading-5">{{ $t('Approval required') }}</h3>
9
- <p class="mt-1 text-sm leading-5 text-lightListTableText dark:text-darkListTableText">
14
+ <h3 class="text-sm font-semibold leading-5 text-lightCardTitle dark:text-darkCardTitle">{{ $t('Approval needed') }}</h3>
15
+ <p class="text-[0.8125rem] leading-5 text-lightCardDescription dark:text-darkCardDescription">
10
16
  {{ $t('Review what the agent is about to do before continuing.') }}
11
17
  </p>
12
- <ul
13
- v-if="data.messages?.length"
14
- class="mt-3 space-y-1 text-sm leading-5 text-lightListTableHeadingText dark:text-darkListTableHeadingText"
18
+ </div>
19
+ </div>
20
+ <ul
21
+ class="mt-3 divide-y rounded-default border text-[0.8125rem] leading-5
22
+ divide-lightCardBorder border-lightCardBorder bg-lightCardBackground text-lightTableText
23
+ dark:divide-darkCardBorder dark:border-darkCardBorder dark:bg-darkCardBackground dark:text-darkTableText"
24
+ >
25
+ <li
26
+ v-for="(approval, approvalIndex) in data.approvals"
27
+ :key="approvalIndex"
28
+ class="px-3 py-2.5"
29
+ >
30
+ <p class="font-medium [overflow-wrap:anywhere]">{{ approval.summary }}</p>
31
+ <dl
32
+ v-if="approval.fields.length"
33
+ class="mt-2 grid grid-cols-[minmax(0,auto)_minmax(0,1fr)] gap-x-4 gap-y-1.5"
15
34
  >
16
- <li
17
- v-for="message in data.messages"
18
- :key="message"
19
- class="flex gap-2"
20
- >
21
- <span class="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-lightListTableHeadingText dark:bg-darkListTableHeadingText" />
22
- <span class="min-w-0 whitespace-pre-line break-words">{{ message }}</span>
23
- </li>
24
- </ul>
25
- <div class="mt-4 flex flex-wrap gap-2">
26
- <template v-if="data.status === 'pending'">
27
- <button
28
- type="button"
29
- class="inline-flex h-9 items-center gap-2 rounded-md border border-lightButtonsBorder bg-lightButtonsBackground px-3 text-sm font-medium text-lightButtonsText transition hover:bg-lightButtonsHover dark:border-darkButtonsBorder dark:bg-darkButtonsBackground dark:text-darkButtonsText dark:hover:bg-darkButtonsHover"
30
- @click="submit('approve')"
31
- >
32
- <IconCheckOutline class="h-4 w-4" />
33
- {{ $t('Approve') }}
34
- </button>
35
- <button
36
- type="button"
37
- class="inline-flex h-9 items-center gap-2 rounded-md border border-lightListTableText/20 bg-transparent px-3 text-sm font-medium text-lightListTableHeadingText transition hover:bg-lightListTableText/10 dark:border-darkListTableText/20 dark:text-darkListTableHeadingText dark:hover:bg-darkListTableText/10"
38
- @click="submit('reject')"
39
- >
40
- <IconCloseOutline class="h-4 w-4" />
41
- {{ $t('Reject') }}
42
- </button>
35
+ <template v-for="(field, fieldIndex) in approval.fields" :key="fieldIndex">
36
+ <dt class="text-lightInputIconColor dark:text-darkInputIconColor [overflow-wrap:anywhere]">{{ field.label }}</dt>
37
+ <dd class="flex min-w-0 flex-wrap items-center gap-1.5">
38
+ <template v-if="field.from !== undefined && field.from !== field.to">
39
+ <span
40
+ class="min-w-0 rounded px-1.5 line-through [overflow-wrap:anywhere]
41
+ bg-lightDangerSoft text-lightDanger decoration-lightDanger/50 dark:bg-darkDangerSoft dark:text-darkDanger dark:decoration-darkDanger/50"
42
+ >
43
+ {{ field.from }}
44
+ </span>
45
+ <IconArrowRight class="h-3.5 w-3.5 shrink-0 text-lightInputIconColor dark:text-darkInputIconColor" />
46
+ <span
47
+ class="min-w-0 rounded px-1.5 font-medium [overflow-wrap:anywhere]
48
+ bg-lightSuccessSoft text-lightSuccess dark:bg-darkSuccessSoft dark:text-darkSuccess"
49
+ >
50
+ {{ field.to }}
51
+ </span>
52
+ </template>
53
+ <span v-else class="min-w-0 font-medium [overflow-wrap:anywhere]">{{ field.to }}</span>
54
+ </dd>
43
55
  </template>
44
- <span
45
- v-else
46
- class="inline-flex h-8 items-center gap-2 rounded-md border border-lightListTableText/20 bg-lightListTableText/10 px-2.5 text-sm font-medium text-lightListTableHeadingText dark:border-darkListTableText/20 dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText"
47
- >
48
- <Spinner v-if="data.status === 'processing'" class="h-4 w-4" />
49
- <IconCheckOutline v-else-if="data.status === 'approved'" class="h-4 w-4" />
50
- <IconCloseOutline v-else class="h-4 w-4" />
51
- {{ data.status === 'processing' ? $t('Processing') : data.status === 'approved' ? $t('Approved') : $t('Rejected') }}
52
- </span>
53
- </div>
54
- <p
55
- v-if="data.error"
56
- class="mt-3 text-sm leading-5 text-red-600 dark:text-red-400"
57
- >
58
- {{ data.error }}
59
- </p>
60
- </div>
56
+ </dl>
57
+ </li>
58
+ </ul>
59
+ <div class="mt-3 flex flex-wrap gap-2">
60
+ <template v-if="data.status === 'pending'">
61
+ <Button size="sm" type="button" @click="submit('approve')">
62
+ <IconCheck class="h-4 w-4" />
63
+ {{ $t('Approve') }}
64
+ </Button>
65
+ <Button variant="secondary" size="sm" type="button" @click="submit('reject')">
66
+ {{ $t('Reject') }}
67
+ </Button>
68
+ </template>
69
+ <span
70
+ v-else
71
+ class="inline-flex h-6 items-center gap-1.5 rounded-full border px-2 text-xs font-medium"
72
+ :class="STATUS_BADGE_CLASSES[data.status]"
73
+ >
74
+ <Spinner v-if="data.status === 'processing'" class="h-3.5 w-3.5" />
75
+ <IconCheck v-else-if="data.status === 'approved'" class="h-3.5 w-3.5" />
76
+ <IconX v-else class="h-3.5 w-3.5" />
77
+ {{ data.status === 'processing' ? $t('Processing') : data.status === 'approved' ? $t('Approved') : $t('Rejected') }}
78
+ </span>
61
79
  </div>
80
+ <p
81
+ v-if="data.error"
82
+ class="mt-3 text-sm leading-5 text-lightDanger dark:text-darkDanger"
83
+ >
84
+ {{ data.error }}
85
+ </p>
62
86
  </div>
63
87
  </template>
64
88
 
65
89
  <script setup lang="ts">
90
+ import { computed } from 'vue';
66
91
  import type { IPartData } from '../types';
67
92
  import { useAgentStore } from '../composables/useAgentStore';
68
- import { Spinner } from '@/afcl';
69
- import { IconCheckOutline, IconCloseOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
93
+ import { Button, Spinner } from '@/afcl';
94
+ import { IconArrowRight, IconCheck, IconShieldAlert, IconX } from '@iconify-prerendered/vue-lucide';
70
95
 
71
96
  const props = defineProps<{
72
97
  data: IPartData;
73
98
  }>();
74
99
 
100
+ const STATUS_BADGE_CLASSES: Record<string, string> = {
101
+ processing: 'border-transparent bg-lightSecondaryDarken text-lightInputIconColor dark:bg-darkSecondaryLighten dark:text-darkInputIconColor',
102
+ approved: 'border-lightSuccessBorder bg-lightSuccessSoft text-lightSuccess dark:border-darkSuccessBorder dark:bg-darkSuccessSoft dark:text-darkSuccess',
103
+ rejected: 'border-lightDangerBorder bg-lightDangerSoft text-lightDanger dark:border-darkDangerBorder dark:bg-darkDangerSoft dark:text-darkDanger',
104
+ };
105
+
75
106
  const agentStore = useAgentStore();
76
107
 
108
+ const isAwaitingDecision = computed(() => props.data.status === 'pending' || props.data.status === 'processing');
109
+
77
110
  function submit(decision: 'approve' | 'reject') {
78
111
  if (!props.data.sessionId || props.data.status !== 'pending') {
79
112
  return;
@@ -1,68 +1,56 @@
1
1
  <template>
2
- <div
3
- ref="toolRendererRef"
4
- class="py-1 inline-flex justify-center m-2 max-w-full min-w-0
5
- flex-col gap-3 rounded-xl px-2 text-lightListTableHeadingText
6
- dark:text-darkListTableHeadingText select-none
7
- "
8
- :class="[
9
- isInputOutputExpanded ? 'items-start border-none' : '',
10
- activateShrinkedStyle ? 'border items-center' : '',
11
- activateFullWidth ? 'w-full' : '',
12
- ]"
13
- :style="{
14
- maxWidth: activateShrinkedStyle ? toolRendererInitialWidth + 'px' : '',
15
- }"
16
- >
17
- <div
18
- class="flex items-center gap-1 cursor-pointer"
19
- @click="toggleInputOutput()"
2
+ <li>
3
+ <button
4
+ type="button"
5
+ class="flex items-center gap-2 w-full py-0.5 text-left text-[0.8125rem] leading-5 select-none"
6
+ :class="hasToolSections ? 'cursor-pointer' : 'cursor-default'"
7
+ @click="hasToolSections && (isInputOutputExpanded = !isInputOutputExpanded)"
20
8
  >
21
- <div class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-lightNavbar dark:bg-darkNavbar">
22
- <Spinner v-if="isRunning" class="h-4 w-4" />
23
- <IconCheckOutline v-else-if="!props.data.toolInfo.error" class="h-4 w-4 text-lightPrimary dark:text-darkPrimary" />
24
- <IconCloseOutline v-else class="h-4 w-4 text-red-500" />
25
- </div>
26
-
27
- <div class="min-w-0">
28
- <p class="break-words font-mono text-sm leading-5">
29
- {{ props.data?.toolInfo?.toolInfo ? props.data.toolInfo.toolInfo : props.data?.toolInfo?.toolName}}
30
- </p>
31
- </div>
32
- <IconAngleDownOutline
9
+ <span class="flex items-center justify-center w-4 h-4 shrink-0 rounded-full" :class="statusClasses">
10
+ <Spinner v-if="isRunning" class="w-3 h-3" />
11
+ <IconX v-else-if="props.data.toolInfo.error" class="w-3 h-3" />
12
+ <IconCheck v-else class="w-3 h-3" />
13
+ </span>
14
+ <span class="max-w-[55%] shrink-0 truncate font-mono text-xs text-lightInputText dark:text-darkInputText">
15
+ {{ props.data.toolInfo.toolName }}
16
+ </span>
17
+ <span class="flex-1 min-w-0 truncate text-lightInputIconColor dark:text-darkInputIconColor" :title="props.data.toolInfo.toolInfo">
18
+ {{ props.data.toolInfo.toolInfo }}
19
+ </span>
20
+ <span v-if="durationLabel" class="shrink-0 tabular-nums text-lightInputIconColor dark:text-darkInputIconColor">{{ durationLabel }}</span>
21
+ <IconChevronDown
33
22
  v-if="hasToolSections"
23
+ class="w-3.5 h-3.5 shrink-0 text-lightInputIconColor transition-transform duration-200 dark:text-darkInputIconColor"
34
24
  :class="isInputOutputExpanded ? 'rotate-180' : 'rotate-0'"
35
- @transitionend="finishTransition()"
36
- class="shrink-0 cursor-pointer transition-transform duration-300 hover:scale-105"
37
25
  />
38
- </div>
26
+ </button>
39
27
  <transition name="expand">
40
- <div v-if="isInputOutputExpanded" v-show="isInputOutputExpanded" class="max-h-72 space-y-3 overflow-y-auto pr-1 w-full">
28
+ <div v-if="isInputOutputExpanded" class="max-h-72 mt-1.5 mb-1 ml-6 space-y-2 overflow-y-auto">
41
29
  <section
42
30
  v-for="section in toolSections"
43
31
  :key="section.label"
44
- class="overflow-hidden rounded-xl border border-black/5 bg-white/70 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-slate-900/50"
32
+ class="overflow-hidden rounded-default border border-lightCardBorder bg-lightCardBackground dark:border-darkCardBorder dark:bg-darkCardBackground"
45
33
  >
46
- <header class="border-b border-black/5 px-3 py-2 text-[0.7rem] font-semibold uppercase tracking-[0.22em] text-gray-500 dark:border-white/10 dark:text-gray-400">
34
+ <header class="border-b border-lightCardBorder px-2.5 py-1.5 text-xs font-medium text-lightInputIconColor dark:border-darkCardBorder dark:text-darkInputIconColor">
47
35
  {{ section.label }}
48
36
  </header>
49
- <div class="select-all grid grid-cols-[auto,1fr] gap-x-3 gap-y-1 px-3 py-3 font-mono text-xs leading-5 text-gray-700 dark:text-gray-200">
37
+ <div class="select-all grid grid-cols-[auto,1fr] gap-x-3 gap-y-0.5 px-2.5 py-2 font-mono text-xs leading-5 text-lightTableText dark:text-darkTableText">
50
38
  <template v-for="line in section.lines" :key="`${section.label}-${line.number}`">
51
- <span class=" text-[0.7rem] text-gray-400 dark:text-gray-500 select-none">{{ line.number }}</span>
39
+ <span class="text-[0.7rem] text-lightInputPlaceholderText dark:text-darkInputPlaceholderText select-none">{{ line.number }}</span>
52
40
  <span class="whitespace-pre-wrap break-words">{{ line.content || ' ' }}</span>
53
41
  </template>
54
42
  </div>
55
43
  </section>
56
44
  </div>
57
45
  </transition>
58
- </div>
46
+ </li>
59
47
  </template>
60
48
 
61
49
  <script setup lang="ts">
62
- import { computed, ref, watch, onMounted, nextTick } from 'vue';
50
+ import { computed, ref } from 'vue';
63
51
  import { type IFormattedToolCallPart } from '../types';
64
52
  import { Spinner } from '@/afcl';
65
- import { IconAngleDownOutline, IconCheckOutline, IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
53
+ import { IconCheck, IconChevronDown, IconX } from '@iconify-prerendered/vue-lucide';
66
54
 
67
55
  const props = defineProps<{
68
56
  data: IFormattedToolCallPart
@@ -77,14 +65,20 @@
77
65
  }
78
66
 
79
67
  const isInputOutputExpanded = ref(false);
80
- const activateShrinkedStyle = ref(true);
81
- const toolRendererInitialWidth = ref<number | null>(null);
82
- const toolRendererRef = ref<HTMLElement | null>(null);
83
- const activateFullWidth = ref(false);
84
- const blockClicksDuringAnimation = ref(false);
85
- const ANIMATION_DURATION = 300;
86
-
87
- const isRunning = computed(() => props.data?.toolInfo?.phase === 'start');
68
+
69
+ const isRunning = computed(() => props.data.toolInfo.phase === 'start');
70
+ const durationLabel = computed(() => {
71
+ const { durationMs } = props.data.toolInfo;
72
+ return durationMs === undefined ? null : `${(durationMs / 1000).toFixed(1)}s`;
73
+ });
74
+ const statusClasses = computed(() => {
75
+ if (isRunning.value) {
76
+ return 'text-lightInputIconColor dark:text-darkInputIconColor';
77
+ }
78
+ return props.data.toolInfo.error
79
+ ? 'bg-lightDangerSoft text-lightDanger dark:bg-darkDangerSoft dark:text-darkDanger'
80
+ : 'bg-lightSuccessSoft text-lightSuccess dark:bg-darkSuccessSoft dark:text-darkSuccess';
81
+ });
88
82
  const toolSections = computed<IToolSection[]>(() => {
89
83
  const sections = [
90
84
  {
@@ -109,38 +103,6 @@
109
103
  });
110
104
  const hasToolSections = computed(() => toolSections.value.length > 0);
111
105
 
112
- onMounted(async () => {
113
- await nextTick();
114
- if (toolRendererRef.value && props.data.toolInfo) {
115
- const contentWidth = Math.ceil(toolRendererRef.value.getBoundingClientRect().width);
116
- const parentWidth = toolRendererRef.value.parentElement?.clientWidth ?? Infinity;
117
- toolRendererInitialWidth.value = Math.min(contentWidth, parentWidth);
118
- }
119
- });
120
-
121
- watch(isInputOutputExpanded, (newValue) => {
122
- if (newValue) {
123
- activateShrinkedStyle.value = false;
124
- activateFullWidth.value = true;
125
- }
126
- });
127
-
128
- function finishTransition() {
129
- if (!isInputOutputExpanded.value) {
130
- activateFullWidth.value = false;
131
- activateShrinkedStyle.value = true;
132
- }
133
- }
134
-
135
- function toggleInputOutput() {
136
- if (blockClicksDuringAnimation.value) return;
137
- isInputOutputExpanded.value = !isInputOutputExpanded.value;
138
- blockClicksDuringAnimation.value = true;
139
- setTimeout(() => {
140
- blockClicksDuringAnimation.value = false;
141
- }, ANIMATION_DURATION);
142
- }
143
-
144
106
  const normalizeToolPayload = (value: unknown): string | null => {
145
107
  if (value === null || value === undefined || value === '') {
146
108
  return null;
@@ -175,4 +137,4 @@
175
137
  max-height: 288px;
176
138
  }
177
139
 
178
- </style>
140
+ </style>
@@ -205,6 +205,9 @@ async function renderHighlight() {
205
205
  const result = await embed(vegaContainer.value, spec, {
206
206
  actions: false,
207
207
  renderer: 'svg',
208
+ // the chart sits on the surface of the code block, so it takes the theme of the page
209
+ theme: codeTheme.value === 'dark' ? 'dark' : undefined,
210
+ config: { background: 'transparent' },
208
211
  });
209
212
 
210
213
  if (requestId !== renderRequestId) {
@@ -259,70 +262,36 @@ function clearVega() {
259
262
 
260
263
  <style scoped>
261
264
  .incremark-shiki-code {
262
- --incremark-code-border: rgba(148, 163, 184, 0.24);
263
- --incremark-code-toolbar-bg: rgba(248, 250, 252, 0.9);
264
- --incremark-code-toolbar-text: #475569;
265
- --incremark-code-copy-bg: rgba(226, 232, 240, 0.9);
266
- --incremark-code-copy-text: #0f172a;
267
- border: 1px solid var(--incremark-code-border);
268
- border-radius: 16px;
265
+ @apply border border-lightCardBorder bg-lightCardBackground dark:border-darkCardBorder dark:bg-darkCardBackground;
266
+ border-radius: 8px;
269
267
  overflow: hidden;
270
268
  width: 100%;
271
- background:
272
- linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.96));
273
- box-shadow: 0 14px 30px -24px rgba(15, 23, 42, 0.45);
274
- margin: 16px 0 ;
275
- }
276
-
277
- .dark .incremark-shiki-code {
278
- --incremark-code-border: rgba(100, 116, 139, 0.38);
279
- --incremark-code-toolbar-bg: rgba(15, 23, 42, 0.88);
280
- --incremark-code-toolbar-text: #cbd5e1;
281
- --incremark-code-copy-bg: rgba(30, 41, 59, 0.88);
282
- --incremark-code-copy-text: #e2e8f0;
283
- background:
284
- linear-gradient(180deg, rgba(2, 6, 23, 0.96), rgba(15, 23, 42, 0.98));
285
- }
286
-
287
- .incremark-shiki-code[data-stream-state='pending'],
288
- .incremark-shiki-code[data-stream-state='stable'] {
289
- box-shadow: 0 10px 24px -24px rgba(15, 23, 42, 0.8);
269
+ margin: 0 0 8px;
290
270
  }
291
271
 
292
272
  .incremark-shiki-toolbar {
273
+ @apply border-b border-lightCardBorder bg-lightCardBackgroundHover dark:border-darkCardBorder dark:bg-darkCardBackgroundHover;
293
274
  display: flex;
294
275
  align-items: center;
295
276
  justify-content: space-between;
296
277
  gap: 12px;
297
- padding: 10px 14px;
298
- border-bottom: 1px solid var(--incremark-code-border);
299
- background: var(--incremark-code-toolbar-bg);
300
- backdrop-filter: blur(10px);
278
+ padding: 4px 4px 4px 12px;
301
279
  }
302
280
 
303
281
  .incremark-shiki-language {
304
- font-size: 11px;
305
- font-weight: 600;
306
- letter-spacing: 0.12em;
307
- text-transform: uppercase;
308
- color: var(--incremark-code-toolbar-text);
282
+ @apply text-xs font-medium text-lightInputIconColor dark:text-darkInputIconColor;
309
283
  }
310
284
 
311
285
  .incremark-shiki-copy {
312
- border: none;
313
- border-radius: 999px;
314
- padding: 6px 10px;
315
- font-size: 11px;
316
- font-weight: 600;
317
- letter-spacing: 0.04em;
318
- background: var(--incremark-code-copy-bg);
319
- color: var(--incremark-code-copy-text);
320
- cursor: pointer;
321
- transition: transform 0.16s ease, opacity 0.16s ease, background-color 0.16s ease;
286
+ @apply h-6 rounded-default px-2 text-xs font-medium transition-colors text-lightInputIconColor dark:text-darkInputIconColor;
322
287
  }
323
288
 
324
289
  .incremark-shiki-copy:hover:not(:disabled) {
325
- transform: translateY(-1px);
290
+ @apply bg-lightSecondaryDarken text-lightInputText dark:bg-darkSecondaryLighten dark:text-darkInputText;
291
+ }
292
+
293
+ .incremark-shiki-copy:focus-visible {
294
+ @apply outline-none ring-[3px] ring-lightFocusRing dark:ring-darkFocusRing;
326
295
  }
327
296
 
328
297
  .incremark-shiki-copy:disabled {
@@ -331,13 +300,7 @@ function clearVega() {
331
300
  }
332
301
 
333
302
  .incremark-shiki-copy.is-copied {
334
- background: rgba(16, 185, 129, 0.16);
335
- color: #059669;
336
- }
337
-
338
- .dark .incremark-shiki-copy.is-copied {
339
- background: rgba(16, 185, 129, 0.24);
340
- color: #6ee7b7;
303
+ @apply text-lightSuccess dark:text-darkSuccess;
341
304
  }
342
305
 
343
306
  .incremark-shiki-body {
@@ -345,13 +308,13 @@ function clearVega() {
345
308
  }
346
309
 
347
310
  .incremark-vega {
348
- padding: 18px;
311
+ padding: 12px;
349
312
  width: 100%;
350
313
  }
351
314
 
352
315
  .incremark-shiki-fallback {
353
316
  margin: 0;
354
- padding: 18px;
317
+ padding: 12px;
355
318
  overflow-x: auto;
356
319
  background: transparent;
357
320
  color: inherit;
@@ -359,7 +322,7 @@ function clearVega() {
359
322
 
360
323
  .incremark-shiki-fallback code {
361
324
  display: block;
362
- font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
325
+ font-family: theme('fontFamily.mono');
363
326
  font-size: 13px;
364
327
  line-height: 1.65;
365
328
  white-space: pre;
@@ -367,7 +330,7 @@ function clearVega() {
367
330
 
368
331
  :deep(.incremark-shiki-html pre.shiki) {
369
332
  margin: 0;
370
- padding: 18px;
333
+ padding: 12px;
371
334
  overflow-x: auto;
372
335
  background: transparent !important;
373
336
  font-size: 13px;
@@ -376,7 +339,7 @@ function clearVega() {
376
339
 
377
340
  :deep(.incremark-shiki-html code) {
378
341
  display: block;
379
- font-family: 'JetBrains Mono', 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
342
+ font-family: theme('fontFamily.mono');
380
343
  }
381
344
 
382
345
  :deep(.incremark-shiki-html .line) {
@@ -387,7 +350,5 @@ function clearVega() {
387
350
  width: 100%;
388
351
  }
389
352
 
390
- :deep(.incremark-vega){
391
- padding: 0;
392
- }
353
+
393
354
  </style>
@@ -2,10 +2,9 @@
2
2
  <div
3
3
  v-for="(height, index) in lineHeights"
4
4
  :key="index"
5
- class="bg-white w-[0.2rem] rounded-sm transition-all duration-100 ease-out"
5
+ class="bg-current w-[0.2rem] rounded-sm transition-all duration-100 ease-out"
6
6
  :style="{ height }"
7
7
  />
8
- <p v-if="isRecording" class="text-white ml-2">{{ $t('End') }}</p>
9
8
  </template>
10
9
 
11
10
  <script setup lang="ts">
@@ -1,40 +1,37 @@
1
1
  <template>
2
2
  <div
3
3
  v-show="!agentStore.isResponseInProgress || isAudioChatMode"
4
- class="absolute bottom-1 flex items-center justify-center z-10 gap-4"
5
- :class="[!agentStore.isAudioChatMode ? 'right-16': agentStore.isMobile ? 'right-1/2 translate-x-1/2' : 'right-1/2 translate-x-1/2']"
6
- >
7
- <button
8
- v-if="isAudioChatMode && microphoneButtonMode === 'generating'"
9
- class="bg-red-500 hover:bg-red-600 dark:bg-red-700 dark:hover:bg-red-800 h-9 flex items-center justify-center rounded-full gap-2 text-white px-4"
10
- @click="stopCurrentGeneration()"
11
- >
12
- <div class="w-3 h-3 bg-white rounded-full"/>
13
- {{ $t('Stop') }}
14
- </button>
4
+ class="flex items-center"
5
+ >
6
+ <!-- One button for the whole voice flow: the icon shows the state, the label what a click does -->
15
7
  <button
16
- v-else
17
- class="h-7 bg-lightPrimary dark:bg-darkPrimary
18
- hover:opacity-90 rounded-full flex items-center justify-center
19
- transition-all duration-300 ease-in-out overflow-hidden"
20
- :class="[isAudioChatMode ? 'w-32 px-2': 'w-7']"
21
- @click="toggleChatMode"
8
+ type="button"
9
+ class="flex h-[2.125rem] shrink-0 items-center justify-center rounded-full transition-colors duration-150
10
+ focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:focus-visible:ring-darkFocusRing"
11
+ :class="isAudioChatMode
12
+ ? `w-40 gap-2 px-3 text-sm font-medium
13
+ bg-lightPrimary/10 text-lightInputText hover:bg-lightPrimary/15
14
+ dark:bg-darkPrimary/20 dark:text-darkInputText dark:hover:bg-darkPrimary/30`
15
+ : `w-[2.125rem] text-lightInputIconColor hover:bg-lightSecondaryDarken hover:text-lightInputText
16
+ dark:text-darkInputIconColor dark:hover:bg-darkSecondaryLighten dark:hover:text-darkInputText`"
17
+ :title="$t(buttonTitle)"
18
+ :aria-label="$t(buttonTitle)"
19
+ @click="isGenerating ? stopCurrentGeneration() : toggleChatMode()"
22
20
  >
23
- <div class="w-5 h-5 flex items-center justify-center">
24
- <div v-if="microphoneButtonMode === 'listen' || microphoneButtonMode === 'off'" class="flex justify-evenly items-center gap-[0.1rem]">
25
- <AudioLines
26
- :showAnimation="showAudioWavesAnimation"
27
- :isRecording="microphoneButtonMode === 'listen'"
28
- :amplitude="audioAmplitude"
29
- />
30
- <Spinner v-if="isPolling" class="ml-2 w-4 h-4 text-lightButtonsText dark:text-darkButtonsText fill-lightButtonsBackground dark:fill-darkPrimary" />
31
- </div>
32
- <div v-else-if="microphoneButtonMode === 'generating'" class="flex items-center justify-center gap-2 text-white text-sm">
33
- <span class="w-3 h-3 bg-white rounded-sm" />
34
- {{ $t('End') }}
35
- </div>
36
- <Spinner v-else class="w-4 h-4 text-lightButtonsText dark:text-darkButtonsText fill-lightButtonsBackground dark:fill-darkPrimary" />
37
- </div>
21
+ <span
22
+ class="flex h-5 items-center gap-[0.1rem]"
23
+ :class="isAudioChatMode ? 'text-lightPrimary dark:text-darkPrimary' : ''"
24
+ >
25
+ <span v-if="isGenerating" class="w-2.5 h-2.5 rounded-sm bg-current" />
26
+ <Spinner v-else-if="isPolling || microphoneButtonMode === 'transcribing'" class="w-4 h-4" />
27
+ <AudioLines
28
+ v-else
29
+ :showAnimation="showAudioWavesAnimation"
30
+ :isRecording="microphoneButtonMode === 'listen'"
31
+ :amplitude="audioAmplitude"
32
+ />
33
+ </span>
34
+ <template v-if="isAudioChatMode">{{ isGenerating ? $t('Stop') : $t('End') }}</template>
38
35
  </button>
39
36
  </div>
40
37
  </template>
@@ -69,6 +66,13 @@ const sendUserRecordDebounced = debounce(() => {
69
66
  const isPolling = ref(false);
70
67
 
71
68
  const isAudioChatMode = computed(() => agentStore.isAudioChatMode);
69
+ const isGenerating = computed(() => microphoneButtonMode.value === 'generating');
70
+ const buttonTitle = computed(() => {
71
+ if (!isAudioChatMode.value) {
72
+ return 'Start voice chat';
73
+ }
74
+ return isGenerating.value ? 'Stop response' : 'End voice chat';
75
+ });
72
76
 
73
77
  onMounted(() => {
74
78
  agentStore.registerOnBeforeChatCloseCallback(async () => {
@@ -1,3 +1,16 @@
1
+ /** One value on the approval card; `from` is the stored value an update replaces. */
2
+ export interface IApprovalField {
3
+ label: string;
4
+ from?: string;
5
+ to: string;
6
+ }
7
+
8
+ /** A tool call awaiting approval, as described by the backend. */
9
+ export interface IApprovalDescription {
10
+ summary: string;
11
+ fields: IApprovalField[];
12
+ }
13
+
1
14
  export interface IPartData {
2
15
  toolCallId?: string;
3
16
  toolName?: string;
@@ -9,7 +22,7 @@ export interface IPartData {
9
22
  durationMs?: number;
10
23
  toolInfo?: string;
11
24
  status?: 'pending' | 'processing' | 'approved' | 'rejected';
12
- messages?: string[];
25
+ approvals?: IApprovalDescription[];
13
26
  error?: string;
14
27
  }
15
28
  export interface IPart {
@@ -24,11 +37,6 @@ export interface IFormattedToolCallPart {
24
37
  toolInfo: IPartData;
25
38
  }
26
39
 
27
- export interface IToolGroup {
28
- title: string;
29
- groupedTools: IFormattedToolCallPart[];
30
- }
31
-
32
40
  export interface IMessage {
33
41
  id?: string;
34
42
  role: 'user' | 'assistant' | 'system';
@@ -47,8 +55,8 @@ export interface IAgentSession {
47
55
  title: string;
48
56
  timestamp: string;
49
57
  messages: IMessage[];
50
- /** Tool calls this session is still waiting for the user to approve, in plain language. */
51
- pendingApprovals?: string[];
58
+ /** Tool calls this session is still waiting for the user to approve. */
59
+ pendingApprovals?: IApprovalDescription[];
52
60
  }
53
61
 
54
62
  export interface ISessionsListItem {