@adminforth/agent 2.9.0 → 2.10.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.
@@ -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
+ <IconXMark16Solid v-else-if="props.data.toolInfo.error" class="w-3 h-3" />
12
+ <IconCheck16Solid v-else class="w-3 h-3" />
13
+ </span>
14
+ <span class="max-w-[55%] shrink-0 truncate font-mono text-xs text-gray-800 dark:text-gray-200">
15
+ {{ props.data.toolInfo.toolName }}
16
+ </span>
17
+ <span class="flex-1 min-w-0 truncate text-gray-500 dark:text-gray-400" :title="props.data.toolInfo.toolInfo">
18
+ {{ props.data.toolInfo.toolInfo }}
19
+ </span>
20
+ <span v-if="durationLabel" class="shrink-0 tabular-nums text-gray-500 dark:text-gray-400">{{ durationLabel }}</span>
21
+ <IconChevronDown
33
22
  v-if="hasToolSections"
23
+ class="w-3.5 h-3.5 shrink-0 text-gray-400 transition-transform duration-200"
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-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900/40"
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-gray-200 px-2.5 py-1.5 text-[0.7rem] font-semibold uppercase tracking-wider text-gray-500 dark:border-gray-700 dark:text-gray-400">
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-gray-700 dark:text-gray-200">
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-gray-400 dark:text-gray-500 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 { IconCheck16Solid, IconChevronDown, IconXMark16Solid } from '@iconify-prerendered/vue-heroicons';
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-gray-500 dark:text-gray-400';
77
+ }
78
+ return props.data.toolInfo.error
79
+ ? 'bg-red-100 text-red-600 dark:bg-red-500/20 dark:text-red-400'
80
+ : 'bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary';
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>
@@ -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,35 @@
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-8 shrink-0 items-center justify-center rounded-full transition-colors duration-150"
10
+ :class="isAudioChatMode
11
+ ? `w-40 gap-2 px-3 text-sm font-medium
12
+ bg-lightPrimary/10 text-gray-800 hover:bg-lightPrimary/15
13
+ dark:bg-darkPrimary/20 dark:text-gray-100 dark:hover:bg-darkPrimary/30`
14
+ : 'w-8 text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'"
15
+ :title="$t(buttonTitle)"
16
+ :aria-label="$t(buttonTitle)"
17
+ @click="isGenerating ? stopCurrentGeneration() : toggleChatMode()"
22
18
  >
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>
19
+ <span
20
+ class="flex h-5 items-center gap-[0.1rem]"
21
+ :class="isAudioChatMode ? 'text-lightPrimary dark:text-darkPrimary' : ''"
22
+ >
23
+ <span v-if="isGenerating" class="w-2.5 h-2.5 rounded-sm bg-current" />
24
+ <Spinner v-else-if="isPolling || microphoneButtonMode === 'transcribing'" class="w-4 h-4" />
25
+ <AudioLines
26
+ v-else
27
+ :showAnimation="showAudioWavesAnimation"
28
+ :isRecording="microphoneButtonMode === 'listen'"
29
+ :amplitude="audioAmplitude"
30
+ />
31
+ </span>
32
+ <template v-if="isAudioChatMode">{{ isGenerating ? $t('Stop') : $t('End') }}</template>
38
33
  </button>
39
34
  </div>
40
35
  </template>
@@ -69,6 +64,13 @@ const sendUserRecordDebounced = debounce(() => {
69
64
  const isPolling = ref(false);
70
65
 
71
66
  const isAudioChatMode = computed(() => agentStore.isAudioChatMode);
67
+ const isGenerating = computed(() => microphoneButtonMode.value === 'generating');
68
+ const buttonTitle = computed(() => {
69
+ if (!isAudioChatMode.value) {
70
+ return 'Start voice chat';
71
+ }
72
+ return isGenerating.value ? 'Stop response' : 'End voice chat';
73
+ });
72
74
 
73
75
  onMounted(() => {
74
76
  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 {
@@ -1,4 +1,5 @@
1
1
  import type { ToolCallEvent } from "./toolCallEvents.js";
2
+ import type { ApprovalDescription } from "../tools/approvalFormatter.js";
2
3
  export type AgentEvent = {
3
4
  type: "turn-started";
4
5
  messageId: string;
@@ -19,8 +20,8 @@ export type AgentEvent = {
19
20
  type: "interrupt";
20
21
  sessionId: string;
21
22
  interrupt: unknown;
22
- /** One plain-language line per pending tool call, for the approval card. */
23
- approvals: string[];
23
+ /** One description per pending tool call, for the approval card. */
24
+ approvals: ApprovalDescription[];
24
25
  } | {
25
26
  type: "steer-applied";
26
27
  count: number;
@@ -1,12 +1,26 @@
1
- import type { IAdminForth } from "adminforth";
1
+ import { type IAdminForth } from "adminforth";
2
2
  /** One tool call awaiting approval, normalized out of the provider interrupt payload. */
3
3
  export type ApprovalRequest = {
4
4
  toolName: string;
5
5
  args: Record<string, unknown>;
6
6
  };
7
+ /** One value on the approval card; `from` is the stored value an update replaces. */
8
+ export type ApprovalField = {
9
+ label: string;
10
+ from?: string;
11
+ to: string;
12
+ };
13
+ /** A tool call awaiting approval, described for the approval card. */
14
+ export type ApprovalDescription = {
15
+ /** What the call does and to which record, e.g. `Update record with id:10 in Cars`. */
16
+ summary: string;
17
+ /** The values the call writes, in the resource's own wording. */
18
+ fields: ApprovalField[];
19
+ };
7
20
  /**
8
- * Describe a tool call awaiting approval in a sentence an admin (not a developer) can
9
- * act on — resource and column labels instead of ids, values instead of raw JSON.
10
- * Falls back to a key/value dump for tool calls it does not recognize.
21
+ * Describe a tool call awaiting approval so an admin (not a developer) can act on it —
22
+ * resource and column labels instead of ids, values instead of raw JSON, and for an
23
+ * update the current value next to each new one. Tool calls it does not recognize are
24
+ * shown as their arguments.
11
25
  */
12
- export declare function formatApprovalRequest(adminforth: IAdminForth, request: ApprovalRequest): string;
26
+ export declare function formatApprovalRequest(adminforth: IAdminForth, request: ApprovalRequest): Promise<ApprovalDescription>;
@@ -1,7 +1,17 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { logger } from "adminforth";
11
+ import { getErrorMessage } from "../shared/errors.js";
1
12
  /** Argument keys that address the call itself rather than the data being changed. */
2
13
  const NON_FIELD_KEYS = new Set(["resourceId", "recordId", "primaryKey", "requiredColumnsToSkip", "meta", "extra"]);
3
14
  const MAX_VALUE_LENGTH = 80;
4
- const MAX_FIELDS = 12;
5
15
  function truncate(text, limit = MAX_VALUE_LENGTH) {
6
16
  return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
7
17
  }
@@ -45,23 +55,46 @@ function formatValue(value, column) {
45
55
  }
46
56
  return truncate(String(value));
47
57
  }
48
- /** `Brand: Tesla, Price: 45000` — the record payload in the resource's own wording. */
49
- function formatRecord(record, resource) {
58
+ /**
59
+ * The record payload as labelled values in the resource's own wording. Every field is
60
+ * listed: the admin approves exactly what will be written. With `oldRecord` each value is
61
+ * paired with the one it replaces.
62
+ */
63
+ function describeFields(record, resource, oldRecord) {
50
64
  var _a;
51
65
  if (record === null || typeof record !== "object" || Array.isArray(record)) {
52
- return "";
66
+ return [];
53
67
  }
54
68
  const columnsByName = new Map(((_a = resource === null || resource === void 0 ? void 0 : resource.columns) !== null && _a !== void 0 ? _a : []).map((column) => [column.name, column]));
55
- const entries = Object.entries(record)
56
- .filter(([key]) => !NON_FIELD_KEYS.has(key));
57
- const shown = entries.slice(0, MAX_FIELDS).map(([name, value]) => {
69
+ return Object.entries(record)
70
+ .filter(([key]) => !NON_FIELD_KEYS.has(key))
71
+ .map(([name, value]) => {
58
72
  const column = columnsByName.get(name);
59
- return `${(column === null || column === void 0 ? void 0 : column.label) || humanize(name)}: ${formatValue(value, column)}`;
73
+ return {
74
+ label: (column === null || column === void 0 ? void 0 : column.label) || humanize(name),
75
+ from: oldRecord ? formatValue(oldRecord[name], column) : undefined,
76
+ to: formatValue(value, column),
77
+ };
78
+ });
79
+ }
80
+ /**
81
+ * The stored record an update would change, read the same way the update endpoint reads it.
82
+ * The preview must never block the approval prompt, so a failed read only drops the old values.
83
+ */
84
+ function loadRecord(adminforth, resource, recordId) {
85
+ return __awaiter(this, void 0, void 0, function* () {
86
+ var _a;
87
+ if (!resource) {
88
+ return undefined;
89
+ }
90
+ try {
91
+ return (_a = yield adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, String(recordId))) !== null && _a !== void 0 ? _a : undefined;
92
+ }
93
+ catch (error) {
94
+ logger.warn(`Approval preview: failed to read record ${String(recordId)} of "${resource.resourceId}": ${getErrorMessage(error)}`);
95
+ return undefined;
96
+ }
60
97
  });
61
- const hidden = entries.length - shown.length;
62
- const lines = hidden > 0 ? [...shown, `and ${hidden} more`] : shown;
63
- // One field per line: a wide record is unreadable as a single run-on sentence.
64
- return lines.join(",\n");
65
98
  }
66
99
  function actionLabel(resource, actionId) {
67
100
  var _a, _b;
@@ -72,35 +105,50 @@ function actionLabel(resource, actionId) {
72
105
  return `"${(action === null || action === void 0 ? void 0 : action.name) || humanize(actionId)}"`;
73
106
  }
74
107
  /**
75
- * Describe a tool call awaiting approval in a sentence an admin (not a developer) can
76
- * act on — resource and column labels instead of ids, values instead of raw JSON.
77
- * Falls back to a key/value dump for tool calls it does not recognize.
108
+ * Describe a tool call awaiting approval so an admin (not a developer) can act on it —
109
+ * resource and column labels instead of ids, values instead of raw JSON, and for an
110
+ * update the current value next to each new one. Tool calls it does not recognize are
111
+ * shown as their arguments.
78
112
  */
79
113
  export function formatApprovalRequest(adminforth, request) {
80
- const { args, toolName } = request;
81
- const resource = findResource(adminforth, args.resourceId);
82
- const label = resourceLabel(resource, args);
83
- switch (toolName) {
84
- case "create_record": {
85
- const fields = formatRecord(args.record, resource);
86
- return `Create a new record in ${label}${fields ? `:\n${fields}` : ""}`;
87
- }
88
- case "update_record": {
89
- const fields = formatRecord(args.record, resource);
90
- return `Update record with id:${formatValue(args.recordId)} in ${label}${fields ? `, set:\n${fields}` : ""}`;
91
- }
92
- case "delete_record":
93
- return `Delete record with id:${formatValue(args.primaryKey)} from ${label} — this cannot be undone`;
94
- case "start_custom_action":
95
- return `Run ${actionLabel(resource, args.actionId)} on record ${formatValue(args.recordId)} in ${label}`;
96
- case "start_custom_bulk_action":
97
- case "start_bulk_action": {
98
- const count = Array.isArray(args.recordIds) ? args.recordIds.length : 0;
99
- return `Run ${actionLabel(resource, args.actionId)} on ${count} record(s) in ${label}`;
114
+ return __awaiter(this, void 0, void 0, function* () {
115
+ const { args, toolName } = request;
116
+ const resource = findResource(adminforth, args.resourceId);
117
+ const label = resourceLabel(resource, args);
118
+ switch (toolName) {
119
+ case "create_record":
120
+ return {
121
+ summary: `Create a new record in ${label}`,
122
+ fields: describeFields(args.record, resource),
123
+ };
124
+ case "update_record":
125
+ return {
126
+ summary: `Update record with id:${formatValue(args.recordId)} in ${label}`,
127
+ fields: describeFields(args.record, resource, yield loadRecord(adminforth, resource, args.recordId)),
128
+ };
129
+ case "delete_record":
130
+ return {
131
+ summary: `Delete record with id:${formatValue(args.primaryKey)} from ${label} — this cannot be undone`,
132
+ fields: [],
133
+ };
134
+ case "start_custom_action":
135
+ return {
136
+ summary: `Run ${actionLabel(resource, args.actionId)} on record ${formatValue(args.recordId)} in ${label}`,
137
+ fields: [],
138
+ };
139
+ case "start_custom_bulk_action":
140
+ case "start_bulk_action": {
141
+ const count = Array.isArray(args.recordIds) ? args.recordIds.length : 0;
142
+ return {
143
+ summary: `Run ${actionLabel(resource, args.actionId)} on ${count} record(s) in ${label}`,
144
+ fields: [],
145
+ };
146
+ }
147
+ default:
148
+ return {
149
+ summary: humanize(toolName),
150
+ fields: describeFields(args, resource),
151
+ };
100
152
  }
101
- default: {
102
- const fields = formatRecord(args, resource);
103
- return `${humanize(toolName)}${fields ? `:\n${fields}` : ""}`;
104
- }
105
- }
153
+ });
106
154
  }
@@ -1,6 +1,7 @@
1
1
  import type { ChatSurfaceAdapter, ChatSurfaceEventSink, ChatSurfaceIncomingMessage, IAdminForth, IAdminForthEndpointHandlerInput } from "adminforth";
2
2
  import type { HandleEditTurnInput, HandleSpeechTurnInput, HandleTurnInput, RunAndPersistAgentResponseInput, RunAndPersistAgentResponseResult } from "../../domain/turnTypes.js";
3
3
  import type { PluginOptions } from "../../types.js";
4
+ import type { ApprovalDescription } from "../../tools/approvalFormatter.js";
4
5
  export type SessionTurn = {
5
6
  id: string;
6
7
  prompt: string;
@@ -22,7 +23,7 @@ export type AgentEndpointsContext = {
22
23
  }>;
23
24
  runAndPersistAgentResponse(input: RunAndPersistAgentResponseInput): Promise<RunAndPersistAgentResponseResult>;
24
25
  getSessionTurns(sessionId: string): Promise<SessionTurn[]>;
25
- getPendingApprovals(sessionId: string): Promise<string[]>;
26
+ getPendingApprovals(sessionId: string): Promise<ApprovalDescription[]>;
26
27
  createNewTurn(sessionId: string, prompt: string, response?: string): Promise<string>;
27
28
  createSystemTurn(sessionId: string, systemMessage: string): Promise<string>;
28
29
  appendSteerToCurrentTurn(sessionId: string, steerText: string): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -1,59 +0,0 @@
1
- <template >
2
- <template v-if="toolGroup.length > 0">
3
- <li class="mb-6 ms-2 z-50">
4
- <span class="text-listTableHeadingText dark:text-darkListTableHeadingText bg-lightNavbar dark:bg-darkNavbar absolute flex items-center justify-center w-5 h-5 bg-brand-softer rounded-full -start-[0.68rem] ring-4 ring-lightNavbar dark:ring-darkNavbar ring-default">
5
- <div class="w-5 h-5 rounded-full flex items-center justify-center">
6
- <IconWrenchSolid class="w-4 h-4" />
7
- </div>
8
- </span>
9
- <h3
10
- class="flex items-center mb-1 text-sm my-2 ml-3 gap-1 text-listTableHeadingText dark:text-darkListTableHeadingText"
11
- >
12
- <span class="font-semibold select-none ">{{ $t('Call tools') }}</span>
13
- </h3>
14
- <div class="flex flex-wrap">
15
- <template v-for="group in props.toolGroup" :key="group.title">
16
- <ToolRenderer
17
- v-if="isExpanded"
18
- v-for="part in group.groupedTools"
19
- :key="part.toolInfo.toolCallId"
20
- :data="part"
21
- />
22
- </template>
23
- </div>
24
- </li>
25
- </template>
26
- </template>
27
-
28
- <script setup lang="ts">
29
- import ToolRenderer from './ToolRenderer.vue';
30
- import type { IToolGroup } from '../types';
31
- import { IconWrenchSolid } from '@iconify-prerendered/vue-heroicons';
32
-
33
- const props = defineProps<{
34
- toolGroup: IToolGroup[]
35
- isExpanded: boolean
36
- }>();
37
-
38
- </script>
39
-
40
- <style scoped>
41
-
42
- .expand-enter-active,
43
- .expand-leave-active {
44
- transition: all 0.3s ease;
45
- }
46
-
47
- .expand-enter-from,
48
- .expand-leave-to {
49
- opacity: 0;
50
- max-height: 0;
51
- }
52
-
53
- .expand-enter-to,
54
- .expand-leave-from {
55
- opacity: 1;
56
- max-height: 288px;
57
- }
58
-
59
- </style>