@datalayer/agent-runtimes 1.0.4 → 1.0.5
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/README.md +34 -0
- package/lib/App.js +1 -1
- package/lib/agents/AgentDetails.d.ts +22 -1
- package/lib/agents/AgentDetails.js +34 -47
- package/lib/api/index.d.ts +0 -1
- package/lib/api/index.js +4 -2
- package/lib/chat/Chat.d.ts +5 -106
- package/lib/chat/Chat.js +4 -4
- package/lib/chat/ChatFloating.d.ts +7 -140
- package/lib/chat/ChatFloating.js +2 -2
- package/lib/chat/ChatPopupStandalone.d.ts +8 -47
- package/lib/chat/ChatPopupStandalone.js +3 -3
- package/lib/chat/ChatSidebar.d.ts +4 -69
- package/lib/chat/ChatSidebar.js +2 -2
- package/lib/chat/ChatStandalone.d.ts +4 -54
- package/lib/chat/ChatStandalone.js +3 -3
- package/lib/chat/base/ChatBase.js +1083 -157
- package/lib/chat/header/ChatHeaderBase.d.ts +11 -6
- package/lib/chat/header/ChatHeaderBase.js +18 -16
- package/lib/chat/indicators/McpStatusIndicator.d.ts +7 -4
- package/lib/chat/indicators/McpStatusIndicator.js +7 -32
- package/lib/chat/indicators/SandboxStatusIndicator.d.ts +4 -1
- package/lib/chat/indicators/SandboxStatusIndicator.js +9 -9
- package/lib/chat/indicators/SkillsStatusIndicator.d.ts +7 -0
- package/lib/chat/indicators/SkillsStatusIndicator.js +88 -0
- package/lib/chat/indicators/index.d.ts +1 -0
- package/lib/chat/indicators/index.js +1 -0
- package/lib/chat/messages/ChatMessageList.d.ts +1 -1
- package/lib/chat/messages/ChatMessageList.js +108 -113
- package/lib/chat/prompt/InputFooter.d.ts +19 -6
- package/lib/chat/prompt/InputFooter.js +71 -18
- package/lib/chat/prompt/InputPrompt.d.ts +3 -1
- package/lib/chat/prompt/InputPrompt.js +4 -4
- package/lib/chat/prompt/InputPromptFooter.js +1 -1
- package/lib/chat/prompt/InputPromptLexical.d.ts +3 -1
- package/lib/chat/prompt/InputPromptLexical.js +12 -5
- package/lib/chat/prompt/InputPromptText.d.ts +3 -1
- package/lib/chat/prompt/InputPromptText.js +2 -2
- package/lib/chat/tools/ToolApprovalBanner.js +1 -1
- package/lib/chat/tools/ToolCallDisplay.d.ts +3 -1
- package/lib/chat/tools/ToolCallDisplay.js +2 -2
- package/lib/chat/usage/TokenUsageBar.js +20 -2
- package/lib/client/AgentRuntimesClientContext.d.ts +53 -0
- package/lib/client/AgentRuntimesClientContext.js +55 -0
- package/lib/client/AgentsMixin.d.ts +0 -18
- package/lib/client/AgentsMixin.js +6 -30
- package/lib/client/IAgentRuntimesClient.d.ts +215 -0
- package/lib/client/IAgentRuntimesClient.js +5 -0
- package/lib/client/SdkAgentRuntimesClient.d.ts +151 -0
- package/lib/client/SdkAgentRuntimesClient.js +134 -0
- package/lib/client/index.d.ts +4 -1
- package/lib/client/index.js +3 -1
- package/lib/components/NotificationEventCard.js +5 -1
- package/lib/config/AgentConfiguration.js +3 -3
- package/lib/context/ContextDistribution.d.ts +3 -1
- package/lib/context/ContextDistribution.js +8 -27
- package/lib/context/ContextInspector.d.ts +3 -1
- package/lib/context/ContextInspector.js +19 -67
- package/lib/context/ContextPanel.d.ts +3 -1
- package/lib/context/ContextPanel.js +104 -64
- package/lib/context/ContextUsage.d.ts +3 -1
- package/lib/context/ContextUsage.js +3 -3
- package/lib/context/CostTracker.d.ts +9 -3
- package/lib/context/CostTracker.js +26 -47
- package/lib/context/CostUsageChart.d.ts +12 -0
- package/lib/context/CostUsageChart.js +378 -0
- package/lib/context/GraphFlowChart.d.ts +16 -0
- package/lib/context/GraphFlowChart.js +182 -0
- package/lib/context/TokenUsageChart.d.ts +8 -1
- package/lib/context/TokenUsageChart.js +349 -211
- package/lib/context/TurnGraphChart.d.ts +39 -0
- package/lib/context/TurnGraphChart.js +538 -0
- package/lib/context/otelWsPool.d.ts +20 -0
- package/lib/context/otelWsPool.js +69 -0
- package/lib/examples/A2UiComponentGalleryExample.d.ts +0 -17
- package/lib/examples/A2UiComponentGalleryExample.js +315 -522
- package/lib/examples/A2UiContactCardExample.d.ts +0 -18
- package/lib/examples/A2UiContactCardExample.js +154 -411
- package/lib/examples/A2UiRestaurantExample.d.ts +0 -30
- package/lib/examples/A2UiRestaurantExample.js +114 -212
- package/lib/examples/A2UiViewerExample.d.ts +0 -18
- package/lib/examples/A2UiViewerExample.js +283 -532
- package/lib/examples/AgUiBackendToolRenderingExample.js +1 -1
- package/lib/examples/AgUiHaikuGenUiExample.d.ts +1 -1
- package/lib/examples/AgUiHaikuGenUiExample.js +1 -1
- package/lib/examples/AgentCheckpointsExample.js +13 -27
- package/lib/examples/AgentCodemodeExample.d.ts +4 -6
- package/lib/examples/AgentCodemodeExample.js +591 -169
- package/lib/examples/AgentEvalsExample.js +12 -16
- package/lib/examples/AgentGuardrailsExample.js +370 -64
- package/lib/examples/AgentHooksExample.d.ts +3 -0
- package/lib/examples/AgentHooksExample.js +104 -0
- package/lib/examples/AgentMCPExample.d.ts +3 -0
- package/lib/examples/AgentMCPExample.js +480 -0
- package/lib/examples/AgentMemoryExample.js +13 -17
- package/lib/examples/AgentMonitoringExample.js +260 -199
- package/lib/examples/AgentNotificationsExample.js +49 -17
- package/lib/examples/AgentOtelExample.js +2 -3
- package/lib/examples/AgentOutputsExample.d.ts +11 -6
- package/lib/examples/AgentOutputsExample.js +382 -81
- package/lib/examples/AgentParametersExample.d.ts +3 -0
- package/lib/examples/AgentParametersExample.js +246 -0
- package/lib/examples/AgentSandboxExample.d.ts +2 -2
- package/lib/examples/AgentSandboxExample.js +68 -40
- package/lib/examples/AgentSkillsExample.js +91 -99
- package/lib/examples/{AgentspecExample.js → AgentSpecsExample.js} +10 -21
- package/lib/examples/AgentSubagentsExample.d.ts +14 -0
- package/lib/examples/AgentSubagentsExample.js +228 -0
- package/lib/examples/AgentToolApprovalsExample.js +29 -557
- package/lib/examples/AgentTriggersExample.js +819 -565
- package/lib/examples/ChatCustomExample.js +11 -24
- package/lib/examples/ChatExample.js +7 -24
- package/lib/examples/CopilotKitLexicalExample.js +2 -1
- package/lib/examples/CopilotKitNotebookExample.js +2 -1
- package/lib/examples/HomeExample.d.ts +15 -0
- package/lib/examples/HomeExample.js +77 -0
- package/lib/examples/Lexical2Example.js +4 -2
- package/lib/examples/{LexicalExample.d.ts → LexicalAgentExample.d.ts} +4 -4
- package/lib/examples/{LexicalExample.js → LexicalAgentExample.js} +65 -16
- package/lib/examples/{LexicalSidebarExample.d.ts → LexicalAgentSidebarExample.d.ts} +5 -5
- package/lib/examples/LexicalAgentSidebarExample.js +261 -0
- package/lib/examples/NotebookAgentExample.d.ts +9 -0
- package/lib/examples/NotebookAgentExample.js +192 -0
- package/lib/examples/{NotebookSidebarExample.d.ts → NotebookAgentSidebarExample.d.ts} +2 -2
- package/lib/examples/NotebookAgentSidebarExample.js +221 -0
- package/lib/examples/{DatalayerNotebookExample.d.ts → NotebookCollaborationExample.d.ts} +4 -4
- package/lib/examples/{DatalayerNotebookExample.js → NotebookCollaborationExample.js} +3 -3
- package/lib/examples/NotebookExample.d.ts +4 -7
- package/lib/examples/NotebookExample.js +14 -146
- package/lib/examples/components/AuthRequiredView.d.ts +6 -0
- package/lib/examples/components/AuthRequiredView.js +33 -0
- package/lib/examples/components/ExampleWrapper.d.ts +7 -0
- package/lib/examples/components/ExampleWrapper.js +25 -6
- package/lib/examples/{ag-ui → components}/haiku/HaikuDisplay.js +1 -1
- package/lib/examples/{ag-ui → components}/haiku/InlineHaikuCard.js +1 -1
- package/lib/examples/{ag-ui → components}/haiku/index.d.ts +1 -1
- package/lib/examples/{ag-ui → components}/haiku/index.js +1 -1
- package/lib/examples/components/index.d.ts +3 -0
- package/lib/examples/components/index.js +4 -0
- package/lib/examples/{ag-ui → components}/weather/index.d.ts +1 -1
- package/lib/examples/{ag-ui → components}/weather/index.js +1 -1
- package/lib/examples/example-selector.d.ts +17 -4
- package/lib/examples/example-selector.js +107 -41
- package/lib/examples/index.d.ts +9 -6
- package/lib/examples/index.js +9 -6
- package/lib/examples/main.js +217 -27
- package/lib/examples/utils/a2ui.d.ts +18 -0
- package/lib/examples/utils/a2ui.js +69 -0
- package/lib/examples/utils/a2uiMarkdownProvider.d.ts +7 -0
- package/lib/examples/utils/a2uiMarkdownProvider.js +9 -0
- package/lib/examples/utils/agentId.d.ts +18 -0
- package/lib/examples/utils/agentId.js +54 -0
- package/lib/examples/utils/agents/earthquake-detector.json +11 -11
- package/lib/examples/utils/agents/sales-forecaster.json +11 -11
- package/lib/examples/utils/agents/social-post-generator.json +11 -11
- package/lib/examples/utils/agents/stock-market.json +11 -11
- package/lib/examples/utils/examplesStore.js +82 -27
- package/lib/hooks/index.d.ts +8 -8
- package/lib/hooks/index.js +7 -7
- package/lib/hooks/useA2A.d.ts +2 -3
- package/lib/hooks/useAIAgentsWebSocket.d.ts +43 -4
- package/lib/hooks/useAIAgentsWebSocket.js +118 -12
- package/lib/hooks/useAcp.d.ts +1 -2
- package/lib/hooks/useAgUi.d.ts +1 -1
- package/lib/hooks/{useAgents.d.ts → useAgentRuntimes.d.ts} +39 -2
- package/lib/hooks/{useAgents.js → useAgentRuntimes.js} +125 -15
- package/lib/hooks/useAgentsCatalog.js +1 -1
- package/lib/hooks/useAgentsService.d.ts +2 -2
- package/lib/hooks/useAgentsService.js +7 -7
- package/lib/hooks/useCheckpoints.js +1 -1
- package/lib/hooks/useConfig.d.ts +4 -1
- package/lib/hooks/useConfig.js +10 -3
- package/lib/hooks/useContextSnapshot.d.ts +9 -4
- package/lib/hooks/useContextSnapshot.js +9 -37
- package/lib/hooks/useMonitoring.js +3 -0
- package/lib/hooks/useSandbox.d.ts +20 -8
- package/lib/hooks/useSandbox.js +105 -40
- package/lib/hooks/useSkills.d.ts +23 -5
- package/lib/hooks/useSkills.js +94 -39
- package/lib/hooks/useToolApprovals.d.ts +60 -36
- package/lib/hooks/useToolApprovals.js +318 -69
- package/lib/hooks/useVercelAI.d.ts +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -0
- package/lib/inference/index.d.ts +0 -1
- package/lib/middleware/index.d.ts +0 -1
- package/lib/protocols/AGUIAdapter.js +6 -0
- package/lib/protocols/VercelAIAdapter.d.ts +7 -0
- package/lib/protocols/VercelAIAdapter.js +59 -7
- package/lib/specs/agents/agents.d.ts +10 -0
- package/lib/specs/agents/agents.js +2139 -262
- package/lib/specs/agents/index.js +3 -1
- package/lib/specs/envvars.d.ts +1 -0
- package/lib/specs/envvars.js +38 -20
- package/lib/specs/evals.js +6 -6
- package/lib/specs/events.d.ts +3 -10
- package/lib/specs/events.js +127 -84
- package/lib/specs/frontendTools.js +2 -2
- package/lib/specs/guardrails.d.ts +0 -7
- package/lib/specs/guardrails.js +240 -159
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/mcpServers.js +35 -6
- package/lib/specs/memory.d.ts +0 -2
- package/lib/specs/memory.js +4 -17
- package/lib/specs/models.js +25 -5
- package/lib/specs/notifications.js +102 -18
- package/lib/specs/outputs.js +15 -9
- package/lib/specs/personas.d.ts +41 -0
- package/lib/specs/personas.js +168 -0
- package/lib/specs/skills.d.ts +2 -1
- package/lib/specs/skills.js +41 -23
- package/lib/specs/teams/index.js +3 -1
- package/lib/specs/teams/teams.js +468 -348
- package/lib/specs/tools.js +4 -4
- package/lib/specs/triggers.js +61 -11
- package/lib/stores/agentRuntimeStore.d.ts +204 -0
- package/lib/stores/agentRuntimeStore.js +636 -0
- package/lib/stores/index.d.ts +1 -1
- package/lib/stores/index.js +1 -1
- package/lib/tools/adapters/copilotkit/lexicalHooks.d.ts +1 -2
- package/lib/tools/adapters/copilotkit/lexicalHooks.js +1 -3
- package/lib/tools/adapters/copilotkit/notebookHooks.d.ts +1 -2
- package/lib/tools/adapters/copilotkit/notebookHooks.js +1 -3
- package/lib/tools/index.d.ts +0 -2
- package/lib/tools/index.js +0 -1
- package/lib/types/agentspecs.d.ts +50 -1
- package/lib/types/chat.d.ts +309 -8
- package/lib/types/context.d.ts +27 -0
- package/lib/types/cost.d.ts +2 -2
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.js +2 -0
- package/lib/types/mcp.d.ts +8 -0
- package/lib/types/models.d.ts +2 -2
- package/lib/types/personas.d.ts +25 -0
- package/lib/types/personas.js +5 -0
- package/lib/types/skills.d.ts +43 -1
- package/lib/types/stream.d.ts +110 -0
- package/lib/types/stream.js +36 -0
- package/lib/utils/utils.d.ts +9 -5
- package/lib/utils/utils.js +9 -5
- package/package.json +13 -9
- package/scripts/codegen/__pycache__/generate_agents.cpython-313.pyc +0 -0
- package/scripts/codegen/__pycache__/generate_events.cpython-313.pyc +0 -0
- package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
- package/scripts/codegen/generate_agents.py +102 -6
- package/scripts/codegen/generate_events.py +35 -13
- package/scripts/codegen/generate_personas.py +319 -0
- package/scripts/codegen/generate_skills.py +9 -9
- package/scripts/sync-jupyter.sh +26 -7
- package/lib/api/tool-approvals.d.ts +0 -62
- package/lib/api/tool-approvals.js +0 -145
- package/lib/examples/LexicalSidebarExample.js +0 -163
- package/lib/examples/NotebookSidebarExample.js +0 -119
- package/lib/examples/NotebookSimpleExample.d.ts +0 -6
- package/lib/examples/NotebookSimpleExample.js +0 -22
- package/lib/examples/ag-ui/index.d.ts +0 -10
- package/lib/examples/ag-ui/index.js +0 -16
- package/lib/hooks/useAgentsRegistry.d.ts +0 -10
- package/lib/hooks/useAgentsRegistry.js +0 -20
- package/lib/stores/agentsStore.d.ts +0 -123
- package/lib/stores/agentsStore.js +0 -270
- /package/lib/examples/{AgentspecExample.d.ts → AgentSpecsExample.d.ts} +0 -0
- /package/lib/examples/{ag-ui → components}/haiku/HaikuDisplay.d.ts +0 -0
- /package/lib/examples/{ag-ui → components}/haiku/InlineHaikuCard.d.ts +0 -0
- /package/lib/examples/{ag-ui → components}/weather/InlineWeatherCard.d.ts +0 -0
- /package/lib/examples/{ag-ui → components}/weather/InlineWeatherCard.js +0 -0
|
@@ -22,19 +22,123 @@ import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
|
22
22
|
import { AlertIcon, PersonIcon } from '@primer/octicons-react';
|
|
23
23
|
import { AiAgentIcon } from '@datalayer/icons-react';
|
|
24
24
|
import { QueryClientProvider, QueryClientContext } from '@tanstack/react-query';
|
|
25
|
+
import { useCoreStore } from '@datalayer/core';
|
|
26
|
+
import { DEFAULT_SERVICE_URLS } from '@datalayer/core/lib/api/constants';
|
|
25
27
|
import { useChatStore } from '../../stores/chatStore';
|
|
26
28
|
import { useConversationStore } from '../../stores/conversationStore';
|
|
27
29
|
import { generateMessageId, createUserMessage, createAssistantMessage, } from '../../types/messages';
|
|
28
30
|
import { internalQueryClient, isToolCallMessage, convertHistoryToDisplayItems, createProtocolAdapter, getApiBaseFromConfig, sanitizeAssistantContent, } from '../../utils';
|
|
29
|
-
import { useConfig, useSkills, useContextSnapshot, useSandbox, } from '../../hooks';
|
|
31
|
+
import { useConfig, useSkills, useSkillActions, useContextSnapshot, useSandbox, } from '../../hooks';
|
|
32
|
+
import { useAgentRuntimeWebSocket } from '../../hooks/useAgentRuntimes';
|
|
33
|
+
import { agentRuntimeStore, useAgentRuntimeStore, useAgentRuntimeWsState, } from '../../stores/agentRuntimeStore';
|
|
30
34
|
import { ChatBaseHeader } from '../header/ChatHeaderBase';
|
|
31
35
|
import { ChatEmptyState } from '../display/EmptyState';
|
|
32
36
|
import { PoweredByTag } from '../display/PoweredByTag';
|
|
33
37
|
import { ChatMessageList, } from '../messages/ChatMessageList';
|
|
34
38
|
import { InputToolbar } from '../prompt/InputFooter';
|
|
39
|
+
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
35
40
|
// Tracks pending prompts already auto-sent for a given conversation scope.
|
|
36
41
|
// This prevents layout-driven unmount/remount cycles from re-sending prompts.
|
|
37
42
|
const sentPendingPromptKeys = new Set();
|
|
43
|
+
const AI_AGENTS_API_PREFIX = '/api/ai-agents/v1';
|
|
44
|
+
const normalizeAgentId = (value) => (value ?? '').trim().toLowerCase();
|
|
45
|
+
const normalizeToolName = (value) => value.replace(/[-_]/g, '').toLowerCase();
|
|
46
|
+
const normalizeSkillApprovalId = (value) => {
|
|
47
|
+
const idx = value.indexOf(':');
|
|
48
|
+
if (idx <= 0)
|
|
49
|
+
return value;
|
|
50
|
+
return value.slice(0, idx);
|
|
51
|
+
};
|
|
52
|
+
const stableStringify = (value) => {
|
|
53
|
+
if (value === null || typeof value !== 'object') {
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
|
58
|
+
}
|
|
59
|
+
const entries = Object.entries(value)
|
|
60
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
61
|
+
.map(([key, itemValue]) => `${JSON.stringify(key)}:${stableStringify(itemValue)}`);
|
|
62
|
+
return `{${entries.join(',')}}`;
|
|
63
|
+
};
|
|
64
|
+
const approvalSignature = (toolName, args) => `${normalizeToolName(toolName)}::${stableStringify(args ?? {})}`;
|
|
65
|
+
const normalizeAiAgentsBaseUrl = (rawBaseUrl) => {
|
|
66
|
+
const trimmed = rawBaseUrl.replace(/\/$/, '');
|
|
67
|
+
if (trimmed.endsWith(AI_AGENTS_API_PREFIX)) {
|
|
68
|
+
return trimmed.slice(0, -AI_AGENTS_API_PREFIX.length);
|
|
69
|
+
}
|
|
70
|
+
return trimmed;
|
|
71
|
+
};
|
|
72
|
+
const toWsUrl = (baseUrl, path, token) => {
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(baseUrl);
|
|
75
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
76
|
+
url.pathname = path;
|
|
77
|
+
if (token) {
|
|
78
|
+
url.searchParams.set('token', token);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
url.search = '';
|
|
82
|
+
}
|
|
83
|
+
return url.toString();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const normalizeApprovalPayload = (input) => {
|
|
90
|
+
const id = (typeof input.id === 'string' && input.id) ||
|
|
91
|
+
(typeof input.approval_id === 'string' && input.approval_id) ||
|
|
92
|
+
(typeof input.approvalId === 'string' && input.approvalId) ||
|
|
93
|
+
'';
|
|
94
|
+
const toolName = (typeof input.tool_name === 'string' && input.tool_name) ||
|
|
95
|
+
(typeof input.toolName === 'string' && input.toolName) ||
|
|
96
|
+
'';
|
|
97
|
+
if (!id || !toolName) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const toolArgs = (input.tool_args && typeof input.tool_args === 'object'
|
|
101
|
+
? input.tool_args
|
|
102
|
+
: input.toolArgs && typeof input.toolArgs === 'object'
|
|
103
|
+
? input.toolArgs
|
|
104
|
+
: undefined) ?? {};
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
tool_name: toolName,
|
|
108
|
+
tool_args: toolArgs,
|
|
109
|
+
tool_call_id: (typeof input.tool_call_id === 'string' && input.tool_call_id) ||
|
|
110
|
+
(typeof input.toolCallId === 'string' && input.toolCallId) ||
|
|
111
|
+
undefined,
|
|
112
|
+
note: (typeof input.note === 'string' && input.note) ||
|
|
113
|
+
(typeof input.message === 'string' && input.message) ||
|
|
114
|
+
undefined,
|
|
115
|
+
status: (typeof input.status === 'string' && input.status) || 'pending',
|
|
116
|
+
agent_id: (typeof input.agent_id === 'string' && input.agent_id) ||
|
|
117
|
+
(typeof input.agentId === 'string' && input.agentId) ||
|
|
118
|
+
undefined,
|
|
119
|
+
created_at: (typeof input.created_at === 'string' && input.created_at) ||
|
|
120
|
+
(typeof input.createdAt === 'string' && input.createdAt) ||
|
|
121
|
+
undefined,
|
|
122
|
+
updated_at: (typeof input.updated_at === 'string' && input.updated_at) ||
|
|
123
|
+
(typeof input.updatedAt === 'string' && input.updatedAt) ||
|
|
124
|
+
undefined,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const isApprovalForAgent = (approval, activeAgentId) => {
|
|
128
|
+
if (!activeAgentId) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (!approval.agent_id) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (approval.agent_id === activeAgentId) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const normalizedApprovalAgentId = normalizeAgentId(approval.agent_id);
|
|
138
|
+
const normalizedActiveAgentId = normalizeAgentId(activeAgentId);
|
|
139
|
+
return (normalizedApprovalAgentId.includes(normalizedActiveAgentId) ||
|
|
140
|
+
normalizedActiveAgentId.includes(normalizedApprovalAgentId));
|
|
141
|
+
};
|
|
38
142
|
function isToolCallOnlyPrompt(content) {
|
|
39
143
|
const normalized = content.toLowerCase();
|
|
40
144
|
return (/tool\s*call\s*only/.test(normalized) ||
|
|
@@ -59,6 +163,88 @@ function formatToolResultFallback(result) {
|
|
|
59
163
|
return 'Tool completed successfully.';
|
|
60
164
|
}
|
|
61
165
|
}
|
|
166
|
+
function extractChatMessagesFromFullContext(fullContext) {
|
|
167
|
+
if (!fullContext) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const rawMessages = Array.isArray(fullContext.messages)
|
|
171
|
+
? fullContext.messages
|
|
172
|
+
: [];
|
|
173
|
+
return rawMessages
|
|
174
|
+
.map((msg, index) => {
|
|
175
|
+
const role = String(msg.role || '').toLowerCase();
|
|
176
|
+
if (role !== 'user' &&
|
|
177
|
+
role !== 'assistant' &&
|
|
178
|
+
role !== 'system' &&
|
|
179
|
+
role !== 'tool') {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const timestampValue = typeof msg.timestamp === 'string' && msg.timestamp.length > 0
|
|
183
|
+
? msg.timestamp
|
|
184
|
+
: new Date().toISOString();
|
|
185
|
+
const createdAt = new Date(timestampValue);
|
|
186
|
+
const content = typeof msg.content === 'string'
|
|
187
|
+
? msg.content
|
|
188
|
+
: JSON.stringify(msg.content ?? '');
|
|
189
|
+
return {
|
|
190
|
+
id: `history-${role}-${index}-${timestampValue}`,
|
|
191
|
+
role,
|
|
192
|
+
content,
|
|
193
|
+
createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,
|
|
194
|
+
};
|
|
195
|
+
})
|
|
196
|
+
.filter((m) => m !== null);
|
|
197
|
+
}
|
|
198
|
+
function parseEnabledMcpToolsByServer(mcpStatusData) {
|
|
199
|
+
if (!mcpStatusData || typeof mcpStatusData !== 'object') {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const raw = mcpStatusData.enabled_tools_by_server;
|
|
203
|
+
if (raw == null) {
|
|
204
|
+
return new Map();
|
|
205
|
+
}
|
|
206
|
+
if (typeof raw !== 'object') {
|
|
207
|
+
return new Map();
|
|
208
|
+
}
|
|
209
|
+
const parsed = new Map();
|
|
210
|
+
for (const [serverId, toolNames] of Object.entries(raw)) {
|
|
211
|
+
if (!Array.isArray(toolNames)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const validToolNames = toolNames.filter((name) => typeof name === 'string' && name.length > 0);
|
|
215
|
+
const normalizedToolNames = validToolNames.map(name => {
|
|
216
|
+
const sep = name.indexOf('__');
|
|
217
|
+
return sep >= 0 ? name.slice(sep + 2) : name;
|
|
218
|
+
});
|
|
219
|
+
parsed.set(serverId, new Set(normalizedToolNames));
|
|
220
|
+
}
|
|
221
|
+
return parsed;
|
|
222
|
+
}
|
|
223
|
+
function parseApprovedMcpToolsByServer(mcpStatusData) {
|
|
224
|
+
if (!mcpStatusData || typeof mcpStatusData !== 'object') {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const raw = mcpStatusData.approved_tools_by_server;
|
|
228
|
+
if (raw == null) {
|
|
229
|
+
return new Map();
|
|
230
|
+
}
|
|
231
|
+
if (typeof raw !== 'object') {
|
|
232
|
+
return new Map();
|
|
233
|
+
}
|
|
234
|
+
const parsed = new Map();
|
|
235
|
+
for (const [serverId, toolNames] of Object.entries(raw)) {
|
|
236
|
+
if (!Array.isArray(toolNames)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const validToolNames = toolNames.filter((name) => typeof name === 'string' && name.length > 0);
|
|
240
|
+
const normalizedToolNames = validToolNames.map(name => {
|
|
241
|
+
const sep = name.indexOf('__');
|
|
242
|
+
return sep >= 0 ? name.slice(sep + 2) : name;
|
|
243
|
+
});
|
|
244
|
+
parsed.set(serverId, new Set(normalizedToolNames));
|
|
245
|
+
}
|
|
246
|
+
return parsed;
|
|
247
|
+
}
|
|
62
248
|
// ---------------------------------------------------------------------------
|
|
63
249
|
// ChatBase (outer wrapper — ensures QueryClient is available)
|
|
64
250
|
// ---------------------------------------------------------------------------
|
|
@@ -100,26 +286,261 @@ export function ChatBase(props) {
|
|
|
100
286
|
// ---------------------------------------------------------------------------
|
|
101
287
|
// ChatBaseInner — contains all actual logic
|
|
102
288
|
// ---------------------------------------------------------------------------
|
|
103
|
-
function ChatBaseInner({ title, showHeader = false, showTokenUsage = true, showLoadingIndicator = true, showErrors = true, showInput = true, showModelSelector =
|
|
289
|
+
function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = true, showLoadingIndicator = true, showErrors = true, showInput = true, showModelSelector = true, showToolsMenu = true, showSkillsMenu = true, disableInputPrompt = false, codemodeEnabled = false, onToggleCodemode, initialModel, availableModels, mcpServers, initialSkills: _initialSkills, className, loadingState, headerActions, chatViewMode, onChatViewModeChange,
|
|
104
290
|
// Mode selection
|
|
105
291
|
useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, enableStreaming = false,
|
|
106
292
|
// Extended props
|
|
107
293
|
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, footerContent, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools,
|
|
294
|
+
// Tool invocation hooks
|
|
295
|
+
onToolCallStart, onToolCallComplete,
|
|
108
296
|
// Identity/Authorization props
|
|
109
|
-
onAuthorizationRequired, connectedIdentities,
|
|
297
|
+
onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
|
|
110
298
|
// Conversation persistence
|
|
111
|
-
runtimeId, historyEndpoint, historyAuthToken,
|
|
299
|
+
runtimeId, historyEndpoint, historyAuthToken: _historyAuthToken,
|
|
112
300
|
// Pending prompt
|
|
113
|
-
pendingPrompt,
|
|
301
|
+
pendingPrompt, contextSnapshot: externalContextSnapshot, mcpStatusData, sandboxStatusData,
|
|
302
|
+
// Tool approval banner
|
|
303
|
+
showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApproveApproval: onApproveApprovalProp, onRejectApproval: onRejectApprovalProp, }) {
|
|
114
304
|
useEffect(() => {
|
|
115
305
|
setupPrimerPortals();
|
|
116
306
|
}, []);
|
|
307
|
+
// ── Built-in pending approvals from the agent-runtime Zustand store ──
|
|
308
|
+
// When the parent doesn't supply the `pendingApprovals` prop, derive them
|
|
309
|
+
// from the shared store so the banner works out-of-the-box.
|
|
310
|
+
const storeApprovals = useAgentRuntimeStore(s => s.approvals);
|
|
311
|
+
const protocolConfig = typeof protocolRaw === 'object'
|
|
312
|
+
? protocolRaw
|
|
313
|
+
: undefined;
|
|
314
|
+
const configuredAiAgentsBaseUrl = useCoreStore((s) => s.configuration?.aiagentsRunUrl);
|
|
315
|
+
const activeAgentId = protocolConfig?.agentId || runtimeId;
|
|
316
|
+
const aiAgentsAuthToken = protocolConfig?.authToken;
|
|
317
|
+
const aiAgentsBaseUrl = useMemo(() => normalizeAiAgentsBaseUrl(configuredAiAgentsBaseUrl || DEFAULT_SERVICE_URLS.AI_AGENTS), [configuredAiAgentsBaseUrl]);
|
|
318
|
+
const aiAgentsApprovalWsRef = useRef(null);
|
|
319
|
+
const storePendingApprovals = useMemo(() => {
|
|
320
|
+
if (pendingApprovalsProp)
|
|
321
|
+
return pendingApprovalsProp;
|
|
322
|
+
return storeApprovals
|
|
323
|
+
.filter(a => a.status === 'pending' && isApprovalForAgent(a, activeAgentId))
|
|
324
|
+
.map(a => ({
|
|
325
|
+
id: a.id,
|
|
326
|
+
toolName: a.tool_name,
|
|
327
|
+
toolDescription: a.note ?? undefined,
|
|
328
|
+
args: a.tool_args ?? {},
|
|
329
|
+
agentId: a.agent_id ?? '',
|
|
330
|
+
requestedAt: a.created_at ?? new Date().toISOString(),
|
|
331
|
+
}));
|
|
332
|
+
}, [pendingApprovalsProp, storeApprovals, activeAgentId]);
|
|
333
|
+
const pendingApprovals = storePendingApprovals;
|
|
334
|
+
// Persist a one-off approval decision into the tools/skills dropdown state
|
|
335
|
+
// on the agent-runtime so the "Approved" toggle moves Off→On across
|
|
336
|
+
// sessions and reloads. For MCP tool approvals (``<server>__<tool>``) this
|
|
337
|
+
// sends ``mcp_server_tool_approve``. For skill approvals
|
|
338
|
+
// (``skill:<skill_id>``) this sends ``skill_approve`` /
|
|
339
|
+
// ``skill_unapprove``.
|
|
340
|
+
const persistApprovalDecision = useCallback((approvalId, approved) => {
|
|
341
|
+
const approval = agentRuntimeStore
|
|
342
|
+
.getState()
|
|
343
|
+
.approvals.find(a => a.id === approvalId);
|
|
344
|
+
const toolName = approval?.tool_name;
|
|
345
|
+
if (!toolName)
|
|
346
|
+
return;
|
|
347
|
+
// Derive the skill id either from a synthetic ``skill:<id>`` tool_name
|
|
348
|
+
// OR from a skill-tool call (``run_skill_script`` / ``load_skill`` /
|
|
349
|
+
// ``read_skill_resource``) carrying ``skill`` / ``skill_name`` / ``name``
|
|
350
|
+
// in its args. This belt-and-suspenders approach makes sure the
|
|
351
|
+
// Approved toggle flips even if the server emits a bare approval
|
|
352
|
+
// request without the ``skill:`` prefix.
|
|
353
|
+
const deriveSkillId = () => {
|
|
354
|
+
if (toolName.startsWith('skill:')) {
|
|
355
|
+
const skillRef = toolName.slice('skill:'.length);
|
|
356
|
+
if (!skillRef)
|
|
357
|
+
return null;
|
|
358
|
+
return normalizeSkillApprovalId(skillRef);
|
|
359
|
+
}
|
|
360
|
+
const SKILL_TOOLS = new Set([
|
|
361
|
+
'run_skill_script',
|
|
362
|
+
'load_skill',
|
|
363
|
+
'read_skill_resource',
|
|
364
|
+
]);
|
|
365
|
+
if (!SKILL_TOOLS.has(toolName))
|
|
366
|
+
return null;
|
|
367
|
+
const a = (approval?.tool_args ?? {});
|
|
368
|
+
const raw = a.skill_name ?? a.skill ?? a.name;
|
|
369
|
+
if (typeof raw !== 'string' || !raw)
|
|
370
|
+
return null;
|
|
371
|
+
return normalizeSkillApprovalId(raw);
|
|
372
|
+
};
|
|
373
|
+
const skillId = deriveSkillId();
|
|
374
|
+
if (skillId) {
|
|
375
|
+
setLocalSkillApproval(prev => {
|
|
376
|
+
const next = new Map(prev);
|
|
377
|
+
next.set(skillId, approved);
|
|
378
|
+
return next;
|
|
379
|
+
});
|
|
380
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
381
|
+
type: approved ? 'skill_approve' : 'skill_unapprove',
|
|
382
|
+
skillId,
|
|
383
|
+
}, activeAgentId);
|
|
384
|
+
if (!ok) {
|
|
385
|
+
console.warn('[ChatBase] skill_approve persistence dropped: websocket not ready');
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// MCP tool approvals: tool_name is ``<serverId>__<toolName>``.
|
|
390
|
+
const sep = toolName.indexOf('__');
|
|
391
|
+
if (sep !== -1) {
|
|
392
|
+
const serverId = toolName.slice(0, sep);
|
|
393
|
+
const toolOnly = toolName.slice(sep + 2);
|
|
394
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
395
|
+
type: 'mcp_server_tool_approve',
|
|
396
|
+
serverId,
|
|
397
|
+
toolName: toolOnly,
|
|
398
|
+
approved,
|
|
399
|
+
}, activeAgentId);
|
|
400
|
+
if (!ok) {
|
|
401
|
+
console.warn('[ChatBase] mcp_server_tool_approve persistence dropped: websocket not ready');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}, [activeAgentId]);
|
|
405
|
+
// Built-in approve/reject: send decisions to the runtime WS only.
|
|
406
|
+
// Approval state updates are sourced from the ai-agents WS listener.
|
|
407
|
+
const onApproveApproval = useCallback(async (approvalId, note, toolCallId) => {
|
|
408
|
+
const approval = agentRuntimeStore
|
|
409
|
+
.getState()
|
|
410
|
+
.approvals.find(a => a.id === approvalId);
|
|
411
|
+
const ok = agentRuntimeStore
|
|
412
|
+
.getState()
|
|
413
|
+
.sendDecision(approvalId, true, note, approval?.tool_call_id ?? toolCallId, activeAgentId);
|
|
414
|
+
if (ok) {
|
|
415
|
+
// Persist decision and clear pending approval only after a successful
|
|
416
|
+
// WS send so failed sends keep the retryable pending UI visible.
|
|
417
|
+
persistApprovalDecision(approvalId, true);
|
|
418
|
+
agentRuntimeStore.getState().removeApproval(approvalId);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
console.warn('[ChatBase] tool_approval_decision dropped: websocket not ready');
|
|
422
|
+
}
|
|
423
|
+
await onApproveApprovalProp?.(approvalId, note);
|
|
424
|
+
}, [activeAgentId, onApproveApprovalProp, persistApprovalDecision]);
|
|
425
|
+
const onRejectApproval = useCallback(async (approvalId, note, toolCallId) => {
|
|
426
|
+
const approval = agentRuntimeStore
|
|
427
|
+
.getState()
|
|
428
|
+
.approvals.find(a => a.id === approvalId);
|
|
429
|
+
const ok = agentRuntimeStore
|
|
430
|
+
.getState()
|
|
431
|
+
.sendDecision(approvalId, false, note, approval?.tool_call_id ?? toolCallId, activeAgentId);
|
|
432
|
+
if (ok) {
|
|
433
|
+
// Keep pending approval visible when send fails, so the user can retry.
|
|
434
|
+
agentRuntimeStore.getState().removeApproval(approvalId);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
console.warn('[ChatBase] tool_approval_decision dropped: websocket not ready');
|
|
438
|
+
}
|
|
439
|
+
await onRejectApprovalProp?.(approvalId, note);
|
|
440
|
+
}, [activeAgentId, onRejectApprovalProp]);
|
|
441
|
+
// Optional ai-agents bridge for server-mode visibility.
|
|
442
|
+
// This keeps approval synchronization in ChatBase so examples do not need
|
|
443
|
+
// their own approval websocket plumbing.
|
|
444
|
+
useEffect(() => {
|
|
445
|
+
if (!showToolApprovalBanner || pendingApprovalsProp) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!aiAgentsAuthToken) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const wsUrl = toWsUrl(aiAgentsBaseUrl, `${AI_AGENTS_API_PREFIX}/ws`, aiAgentsAuthToken);
|
|
452
|
+
if (!wsUrl) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
let closedByCleanup = false;
|
|
456
|
+
const ws = new WebSocket(wsUrl);
|
|
457
|
+
aiAgentsApprovalWsRef.current = ws;
|
|
458
|
+
ws.onopen = () => {
|
|
459
|
+
ws.send(JSON.stringify({ type: 'tool-approvals-history' }));
|
|
460
|
+
};
|
|
461
|
+
ws.onmessage = (event) => {
|
|
462
|
+
try {
|
|
463
|
+
const raw = JSON.parse(String(event.data));
|
|
464
|
+
const records = [];
|
|
465
|
+
const msgType = typeof raw.type === 'string' ? raw.type : undefined;
|
|
466
|
+
const msgEvent = typeof raw.event === 'string' ? raw.event : undefined;
|
|
467
|
+
if (msgType === 'tool-approvals-history') {
|
|
468
|
+
const data = raw.data && typeof raw.data === 'object'
|
|
469
|
+
? raw.data
|
|
470
|
+
: {};
|
|
471
|
+
const approvals = data.approvals;
|
|
472
|
+
if (Array.isArray(approvals)) {
|
|
473
|
+
for (const item of approvals) {
|
|
474
|
+
if (item && typeof item === 'object') {
|
|
475
|
+
records.push(item);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (msgEvent?.startsWith('tool_approval_')) {
|
|
481
|
+
const data = raw.data && typeof raw.data === 'object'
|
|
482
|
+
? raw.data
|
|
483
|
+
: raw.payload && typeof raw.payload === 'object'
|
|
484
|
+
? raw.payload
|
|
485
|
+
: null;
|
|
486
|
+
if (data) {
|
|
487
|
+
records.push(data);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (records.length === 0) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const state = agentRuntimeStore.getState();
|
|
494
|
+
for (const record of records) {
|
|
495
|
+
const approval = normalizeApprovalPayload(record);
|
|
496
|
+
if (!approval) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const scopedApproval = approval.agent_id || !activeAgentId
|
|
500
|
+
? approval
|
|
501
|
+
: { ...approval, agent_id: activeAgentId };
|
|
502
|
+
if (!isApprovalForAgent(scopedApproval, activeAgentId)) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (scopedApproval.status === 'pending') {
|
|
506
|
+
state.upsertApproval(scopedApproval);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
state.removeApproval(scopedApproval.id);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
// Ignore malformed payloads.
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
ws.onclose = () => {
|
|
518
|
+
if (!closedByCleanup) {
|
|
519
|
+
aiAgentsApprovalWsRef.current = null;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
ws.onerror = () => {
|
|
523
|
+
aiAgentsApprovalWsRef.current = null;
|
|
524
|
+
};
|
|
525
|
+
return () => {
|
|
526
|
+
closedByCleanup = true;
|
|
527
|
+
aiAgentsApprovalWsRef.current = null;
|
|
528
|
+
ws.close();
|
|
529
|
+
};
|
|
530
|
+
}, [
|
|
531
|
+
showToolApprovalBanner,
|
|
532
|
+
pendingApprovalsProp,
|
|
533
|
+
aiAgentsBaseUrl,
|
|
534
|
+
aiAgentsAuthToken,
|
|
535
|
+
activeAgentId,
|
|
536
|
+
]);
|
|
117
537
|
// The outer ChatBase wrapper always resolves a string Protocol to a full
|
|
118
538
|
// ProtocolConfig (or undefined). Narrow the type for internal use.
|
|
119
539
|
const protocol = typeof protocolRaw === 'object' ? protocolRaw : undefined;
|
|
120
540
|
// Stabilize the protocol reference so that the adapter-init effect only
|
|
121
541
|
// re-runs when the protocol *contents* actually change.
|
|
122
542
|
const protocolKey = protocol ? JSON.stringify(protocol) : '';
|
|
543
|
+
const monitoringServiceName = 'agent-runtimes';
|
|
123
544
|
// Store (optional for message persistence)
|
|
124
545
|
const clearStoreMessages = useChatStore(state => state.clearMessages);
|
|
125
546
|
// Check if protocol is A2A (doesn't support per-request model override)
|
|
@@ -147,23 +568,94 @@ pendingPrompt, }) {
|
|
|
147
568
|
// enabledTools tracks which MCP server tools are enabled
|
|
148
569
|
// Format: Map<serverId, Set<toolName>>
|
|
149
570
|
const [enabledMcpTools, setEnabledMcpTools] = useState(new Map());
|
|
571
|
+
// approvedMcpTools tracks which MCP server tools are approved per server.
|
|
572
|
+
// Default: no tools approved until explicitly toggled.
|
|
573
|
+
const [approvedMcpTools, setApprovedMcpTools] = useState(new Map());
|
|
150
574
|
// Note: legacy _enabledTools for backend-defined tools from config query
|
|
151
575
|
const [_enabledTools, setEnabledTools] = useState([]);
|
|
152
|
-
|
|
153
|
-
const [enabledSkills, setEnabledSkills] = useState(new Set());
|
|
576
|
+
const wsState = useAgentRuntimeWsState();
|
|
154
577
|
// ---- Data queries ----
|
|
155
|
-
const configQuery = useConfig(Boolean(protocol?.enableConfigQuery), protocol?.configEndpoint, protocol?.authToken);
|
|
578
|
+
const configQuery = useConfig(Boolean(protocol?.enableConfigQuery), protocol?.configEndpoint, protocol?.authToken, protocol?.agentId);
|
|
156
579
|
const skillsQuery = useSkills(Boolean(protocol?.enableConfigQuery) && showSkillsMenu, protocol?.configEndpoint, protocol?.authToken);
|
|
580
|
+
const { enableSkill: wsEnableSkill, disableSkill: wsDisableSkill, approveSkill: wsApproveSkill, unapproveSkill: wsUnapproveSkill, } = useSkillActions(activeAgentId);
|
|
581
|
+
// Optimistic skill-approval overrides so inline approval updates the
|
|
582
|
+
// Skills selector immediately before the next WS snapshot lands.
|
|
583
|
+
const [localSkillApproval, setLocalSkillApproval] = useState(new Map());
|
|
584
|
+
useEffect(() => {
|
|
585
|
+
setLocalSkillApproval(new Map());
|
|
586
|
+
}, [activeAgentId]);
|
|
587
|
+
// Derive enabledSkills from the WS-pushed skill statuses.
|
|
588
|
+
const enabledSkills = useMemo(() => {
|
|
589
|
+
const set = new Set();
|
|
590
|
+
for (const s of skillsQuery.data?.skills ?? []) {
|
|
591
|
+
if (s.status === 'enabled' || s.status === 'loaded') {
|
|
592
|
+
set.add(s.id);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return set;
|
|
596
|
+
}, [skillsQuery.data]);
|
|
597
|
+
// Backward-compatibility bootstrap: if a running agent reports skills as
|
|
598
|
+
// available-only, enable them once. This is gated per agent so explicit
|
|
599
|
+
// user disable actions are not overridden later.
|
|
600
|
+
useEffect(() => {
|
|
601
|
+
if (!activeAgentId) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (defaultSkillsBootstrapRef.current.has(activeAgentId)) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const skills = skillsQuery.data?.skills ?? [];
|
|
608
|
+
if (skills.length === 0) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const hasAnyEnabled = skills.some(s => s.status === 'enabled' || s.status === 'loaded');
|
|
612
|
+
if (hasAnyEnabled) {
|
|
613
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
const availableSkillIds = skills
|
|
617
|
+
.filter(s => s.status === 'available')
|
|
618
|
+
.map(s => s.id)
|
|
619
|
+
.filter(Boolean);
|
|
620
|
+
if (availableSkillIds.length === 0) {
|
|
621
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const allSent = availableSkillIds.every(skillId => wsEnableSkill(skillId));
|
|
625
|
+
if (allSent) {
|
|
626
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
627
|
+
}
|
|
628
|
+
}, [activeAgentId, skillsQuery.data, wsEnableSkill]);
|
|
629
|
+
// Derive approvedSkills from the WS-pushed skill statuses (default: not approved).
|
|
630
|
+
const approvedSkills = useMemo(() => {
|
|
631
|
+
const set = new Set();
|
|
632
|
+
for (const s of skillsQuery.data?.skills ?? []) {
|
|
633
|
+
if (s.approved === true) {
|
|
634
|
+
set.add(s.id);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
localSkillApproval.forEach((approved, skillId) => {
|
|
638
|
+
if (approved) {
|
|
639
|
+
set.add(skillId);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
set.delete(skillId);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
return set;
|
|
646
|
+
}, [localSkillApproval, skillsQuery.data]);
|
|
157
647
|
const contextSnapshotQuery = useContextSnapshot(Boolean(protocol?.enableConfigQuery) && showTokenUsage, protocol?.configEndpoint, protocol?.agentId, protocol?.authToken);
|
|
158
|
-
const agentUsage = contextSnapshotQuery.data;
|
|
159
|
-
const sandboxStatusQuery = useSandbox(Boolean(protocol?.enableConfigQuery) &&
|
|
160
|
-
const sandboxStatus = sandboxStatusQuery.data;
|
|
648
|
+
const agentUsage = externalContextSnapshot ?? contextSnapshotQuery.data;
|
|
649
|
+
const sandboxStatusQuery = useSandbox(Boolean(protocol?.enableConfigQuery) && showHeader, protocol?.configEndpoint, protocol?.authToken, protocol?.agentId);
|
|
161
650
|
// ---- Refs ----
|
|
162
651
|
const adapterRef = useRef(null);
|
|
163
652
|
const unsubscribeRef = useRef(null);
|
|
164
653
|
const toolCallsRef = useRef(new Map());
|
|
165
654
|
const pendingToolExecutionsRef = useRef(0);
|
|
166
655
|
const currentAssistantMessageRef = useRef(null);
|
|
656
|
+
const respondedApprovalIdsRef = useRef(new Set());
|
|
657
|
+
const defaultSkillsBootstrapRef = useRef(new Set());
|
|
658
|
+
const defaultMcpToolsBootstrapRef = useRef(new Set());
|
|
167
659
|
const suppressAssistantTextForToolOnlyRef = useRef(false);
|
|
168
660
|
const hideMessagesAfterToolUIRef = useRef(hideMessagesAfterToolUI);
|
|
169
661
|
hideMessagesAfterToolUIRef.current = hideMessagesAfterToolUI;
|
|
@@ -180,6 +672,77 @@ pendingPrompt, }) {
|
|
|
180
672
|
// re-created when frontendTools changes) always accesses the latest value.
|
|
181
673
|
const frontendToolsRef = useRef(frontendTools);
|
|
182
674
|
frontendToolsRef.current = frontendTools;
|
|
675
|
+
// Stable refs for tool invocation hooks (pre/post)
|
|
676
|
+
const onToolCallStartRef = useRef(onToolCallStart);
|
|
677
|
+
onToolCallStartRef.current = onToolCallStart;
|
|
678
|
+
const onToolCallCompleteRef = useRef(onToolCallComplete);
|
|
679
|
+
onToolCallCompleteRef.current = onToolCallComplete;
|
|
680
|
+
const handleRespondRef = useRef(null);
|
|
681
|
+
const applyServerApprovalDecision = useCallback((approval, approved, note) => {
|
|
682
|
+
if (!approval?.id) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
if (respondedApprovalIdsRef.current.has(approval.id)) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
let targetToolCallId = approval.tool_call_id;
|
|
689
|
+
if (!targetToolCallId) {
|
|
690
|
+
for (const [tcId, tc] of toolCallsRef.current.entries()) {
|
|
691
|
+
if (tc.status !== 'inProgress' && tc.status !== 'executing') {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const tcSig = approvalSignature(tc.toolName, tc.args ?? {});
|
|
695
|
+
const approvalSig = approvalSignature(approval.tool_name, approval.tool_args ?? {});
|
|
696
|
+
if (tcSig === approvalSig) {
|
|
697
|
+
targetToolCallId = tcId;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!targetToolCallId) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const target = toolCallsRef.current.get(targetToolCallId);
|
|
706
|
+
if (!target) {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
if (target.status !== 'inProgress' && target.status !== 'executing') {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
respondedApprovalIdsRef.current.add(approval.id);
|
|
713
|
+
void handleRespondRef.current?.(targetToolCallId, {
|
|
714
|
+
type: 'tool-approval-decision',
|
|
715
|
+
approved,
|
|
716
|
+
approvalId: approval.id,
|
|
717
|
+
toolName: approval.tool_name || target.toolName,
|
|
718
|
+
_fromServerEcho: true,
|
|
719
|
+
_alreadyDispatched: true,
|
|
720
|
+
...(note ? { message: note } : {}),
|
|
721
|
+
});
|
|
722
|
+
return true;
|
|
723
|
+
}, [activeAgentId]);
|
|
724
|
+
// ---- Agent-runtime WebSocket (monitoring stream) ----
|
|
725
|
+
// Derive the bare base URL from configEndpoint or protocol.endpoint.
|
|
726
|
+
const wsBaseUrl = protocol?.configEndpoint
|
|
727
|
+
? protocol.configEndpoint.replace(/\/api\/v1\/(config|configure)\/?$/, '')
|
|
728
|
+
: (protocol?.endpoint?.replace(/\/api\/v1\/.*$/, '') ?? '');
|
|
729
|
+
useAgentRuntimeWebSocket({
|
|
730
|
+
enabled: !!protocol && !!wsBaseUrl,
|
|
731
|
+
baseUrl: wsBaseUrl,
|
|
732
|
+
authToken: protocol?.authToken,
|
|
733
|
+
agentId: protocol?.agentId,
|
|
734
|
+
onMessage: msg => {
|
|
735
|
+
if (msg.type !== 'tool_approval_approved' &&
|
|
736
|
+
msg.type !== 'tool_approval_rejected') {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const payload = msg.payload;
|
|
740
|
+
if (!payload) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
applyServerApprovalDecision(payload, msg.type === 'tool_approval_approved', payload.note ?? undefined);
|
|
744
|
+
},
|
|
745
|
+
});
|
|
183
746
|
// ---- Helpers ----
|
|
184
747
|
const isServerSelected = useCallback((server) => {
|
|
185
748
|
if (!mcpServers)
|
|
@@ -259,7 +822,13 @@ pendingPrompt, }) {
|
|
|
259
822
|
if (server.isAvailable && server.enabled) {
|
|
260
823
|
const shouldEnableServer = isServerSelected(server);
|
|
261
824
|
if (shouldEnableServer) {
|
|
262
|
-
|
|
825
|
+
// Default to "all tools enabled" so the dropdown reflects an
|
|
826
|
+
// immediately-usable state. The WS sync effect will reconcile
|
|
827
|
+
// with server-side state once it arrives, and user toggles
|
|
828
|
+
// win after that.
|
|
829
|
+
const enabledToolNames = new Set(server.tools
|
|
830
|
+
.map(t => t.name)
|
|
831
|
+
.filter((name) => Boolean(name)));
|
|
263
832
|
newEnabledMcpTools.set(server.id, enabledToolNames);
|
|
264
833
|
}
|
|
265
834
|
}
|
|
@@ -283,24 +852,142 @@ pendingPrompt, }) {
|
|
|
283
852
|
const newMap = new Map();
|
|
284
853
|
for (const server of configQuery.data?.mcpServers ?? []) {
|
|
285
854
|
if (isServerSelected(server) && prev.has(server.id)) {
|
|
286
|
-
|
|
855
|
+
const existing = prev.get(server.id);
|
|
856
|
+
if (existing)
|
|
857
|
+
newMap.set(server.id, existing);
|
|
287
858
|
}
|
|
288
859
|
else if (isServerSelected(server) &&
|
|
289
860
|
server.isAvailable &&
|
|
290
861
|
server.enabled) {
|
|
291
|
-
const enabledToolNames = new Set(server.tools
|
|
862
|
+
const enabledToolNames = new Set(server.tools
|
|
863
|
+
.map(t => t.name)
|
|
864
|
+
.filter((name) => Boolean(name)));
|
|
292
865
|
newMap.set(server.id, enabledToolNames);
|
|
293
866
|
}
|
|
294
867
|
}
|
|
295
868
|
return newMap;
|
|
296
869
|
});
|
|
297
870
|
}, [mcpServers, configQuery.data?.mcpServers, isServerSelected]);
|
|
298
|
-
//
|
|
871
|
+
// Keep MCP tool selection synchronized with backend WS snapshots.
|
|
872
|
+
// On first load per agent, if server state reports no enabled tools,
|
|
873
|
+
// bootstrap to "all enabled" from config so codemode starts usable by
|
|
874
|
+
// default. Later user toggles still win because bootstrap runs once.
|
|
875
|
+
const mcpServersRef = useRef(mcpServers);
|
|
876
|
+
mcpServersRef.current = mcpServers;
|
|
877
|
+
useEffect(() => {
|
|
878
|
+
const wsEnabledMcpTools = parseEnabledMcpToolsByServer(mcpStatusData);
|
|
879
|
+
if (!wsEnabledMcpTools) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const bootstrapAgentKey = activeAgentId || '__global__';
|
|
883
|
+
const shouldBootstrap = !defaultMcpToolsBootstrapRef.current.has(bootstrapAgentKey) &&
|
|
884
|
+
wsState === 'connected';
|
|
885
|
+
setEnabledMcpTools(prev => {
|
|
886
|
+
const next = new Map(prev);
|
|
887
|
+
// Apply WS state per server, but only when it carries an authoritative,
|
|
888
|
+
// non-empty list. An empty list from WS is treated as "no information"
|
|
889
|
+
// (likely a transient snapshot before backend defaults are projected)
|
|
890
|
+
// so we keep whatever the user / bootstrap already set, preventing
|
|
891
|
+
// the dropdown from flickering between enabled and disabled states.
|
|
892
|
+
wsEnabledMcpTools.forEach((toolNames, serverId) => {
|
|
893
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
894
|
+
mcpServersRef.current.some(server => server.id === serverId);
|
|
895
|
+
if (!selectedInProps) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (toolNames.size === 0) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
next.set(serverId, new Set(toolNames));
|
|
902
|
+
});
|
|
903
|
+
if (shouldBootstrap) {
|
|
904
|
+
const bootstrapMessages = [];
|
|
905
|
+
for (const server of configQuery.data?.mcpServers ?? []) {
|
|
906
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
907
|
+
mcpServersRef.current.some(s => s.id === server.id);
|
|
908
|
+
if (!selectedInProps || !server.isAvailable || !server.enabled) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
const allToolNames = server.tools
|
|
912
|
+
.map(t => t.name)
|
|
913
|
+
.filter((name) => Boolean(name));
|
|
914
|
+
if (allToolNames.length === 0) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const current = next.get(server.id);
|
|
918
|
+
if (!current || current.size === 0) {
|
|
919
|
+
next.set(server.id, new Set(allToolNames));
|
|
920
|
+
bootstrapMessages.push({
|
|
921
|
+
serverId: server.id,
|
|
922
|
+
enabledToolNames: allToolNames,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
let allMessagesSent = true;
|
|
927
|
+
for (const msg of bootstrapMessages) {
|
|
928
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
929
|
+
type: 'mcp_server_tools_set',
|
|
930
|
+
serverId: msg.serverId,
|
|
931
|
+
enabledToolNames: msg.enabledToolNames,
|
|
932
|
+
}, activeAgentId);
|
|
933
|
+
if (!ok) {
|
|
934
|
+
allMessagesSent = false;
|
|
935
|
+
console.warn('[ChatBase] initial mcp_server_tools_set dropped: websocket not ready');
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (allMessagesSent) {
|
|
939
|
+
defaultMcpToolsBootstrapRef.current.add(bootstrapAgentKey);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return next;
|
|
943
|
+
});
|
|
944
|
+
}, [mcpStatusData, activeAgentId, configQuery.data?.mcpServers, wsState]);
|
|
945
|
+
// Keep MCP tool *approval* synchronized with backend WS snapshots.
|
|
299
946
|
useEffect(() => {
|
|
300
|
-
|
|
301
|
-
|
|
947
|
+
const wsApprovedMcpTools = parseApprovedMcpToolsByServer(mcpStatusData);
|
|
948
|
+
if (!wsApprovedMcpTools) {
|
|
949
|
+
return;
|
|
302
950
|
}
|
|
303
|
-
|
|
951
|
+
setApprovedMcpTools(() => {
|
|
952
|
+
const next = new Map();
|
|
953
|
+
wsApprovedMcpTools.forEach((toolNames, serverId) => {
|
|
954
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
955
|
+
mcpServersRef.current.some(server => server.id === serverId);
|
|
956
|
+
if (selectedInProps) {
|
|
957
|
+
next.set(serverId, new Set(toolNames));
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
return next;
|
|
961
|
+
});
|
|
962
|
+
}, [mcpStatusData]);
|
|
963
|
+
// Refetch configQuery when WS reports MCP servers as started but the
|
|
964
|
+
// cached config response has missing servers or empty tools.
|
|
965
|
+
const lastConfigMcpKeyRef = useRef('');
|
|
966
|
+
useEffect(() => {
|
|
967
|
+
const wsServers = mcpStatusData?.servers;
|
|
968
|
+
if (!wsServers || wsServers.length === 0)
|
|
969
|
+
return;
|
|
970
|
+
const startedIds = wsServers
|
|
971
|
+
.filter(s => s.status === 'started')
|
|
972
|
+
.map(s => s.id)
|
|
973
|
+
.sort();
|
|
974
|
+
if (startedIds.length === 0)
|
|
975
|
+
return;
|
|
976
|
+
const configServers = configQuery.data?.mcpServers || [];
|
|
977
|
+
const needsRefetch = startedIds.some(id => {
|
|
978
|
+
const cs = configServers.find(s => s.id === id);
|
|
979
|
+
return !cs || cs.tools.length === 0;
|
|
980
|
+
});
|
|
981
|
+
// Only refetch once per unique set of started server IDs
|
|
982
|
+
const key = startedIds.join(',');
|
|
983
|
+
if (needsRefetch &&
|
|
984
|
+
key !== lastConfigMcpKeyRef.current &&
|
|
985
|
+
configQuery.refetch) {
|
|
986
|
+
lastConfigMcpKeyRef.current = key;
|
|
987
|
+
configQuery.refetch();
|
|
988
|
+
}
|
|
989
|
+
}, [mcpStatusData, configQuery]);
|
|
990
|
+
// initialSkills are now handled server-side during agent creation.
|
|
304
991
|
// ---- Toggle helpers ----
|
|
305
992
|
const toggleMcpTool = useCallback((serverId, toolName) => {
|
|
306
993
|
setEnabledMcpTools(prev => {
|
|
@@ -313,36 +1000,99 @@ pendingPrompt, }) {
|
|
|
313
1000
|
serverTools.add(toolName);
|
|
314
1001
|
}
|
|
315
1002
|
newMap.set(serverId, serverTools);
|
|
1003
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1004
|
+
type: 'mcp_server_tools_set',
|
|
1005
|
+
serverId,
|
|
1006
|
+
enabledToolNames: Array.from(serverTools),
|
|
1007
|
+
}, activeAgentId);
|
|
1008
|
+
if (!ok) {
|
|
1009
|
+
console.warn('[ChatBase] mcp_server_tools_set dropped: websocket not ready');
|
|
1010
|
+
}
|
|
316
1011
|
return newMap;
|
|
317
1012
|
});
|
|
318
|
-
}, []);
|
|
1013
|
+
}, [activeAgentId]);
|
|
319
1014
|
const toggleAllMcpServerTools = useCallback((serverId, allToolNames, enable) => {
|
|
320
1015
|
setEnabledMcpTools(prev => {
|
|
321
1016
|
const newMap = new Map(prev);
|
|
1017
|
+
const nextTools = enable ? new Set(allToolNames) : new Set();
|
|
322
1018
|
if (enable) {
|
|
323
|
-
newMap.set(serverId,
|
|
1019
|
+
newMap.set(serverId, nextTools);
|
|
324
1020
|
}
|
|
325
1021
|
else {
|
|
326
|
-
newMap.set(serverId,
|
|
1022
|
+
newMap.set(serverId, nextTools);
|
|
1023
|
+
}
|
|
1024
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1025
|
+
type: 'mcp_server_tools_set',
|
|
1026
|
+
serverId,
|
|
1027
|
+
enabledToolNames: Array.from(nextTools),
|
|
1028
|
+
}, activeAgentId);
|
|
1029
|
+
if (!ok) {
|
|
1030
|
+
console.warn('[ChatBase] mcp_server_tools_set dropped: websocket not ready');
|
|
327
1031
|
}
|
|
328
1032
|
return newMap;
|
|
329
1033
|
});
|
|
330
|
-
}, []);
|
|
1034
|
+
}, [activeAgentId]);
|
|
331
1035
|
const toggleSkill = useCallback((skillId) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1036
|
+
if (enabledSkills.has(skillId)) {
|
|
1037
|
+
wsDisableSkill(skillId);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
wsEnableSkill(skillId);
|
|
1041
|
+
}
|
|
1042
|
+
}, [enabledSkills, wsEnableSkill, wsDisableSkill]);
|
|
1043
|
+
const toggleAllSkills = useCallback((allSkillIds, enable) => {
|
|
1044
|
+
for (const id of allSkillIds) {
|
|
1045
|
+
if (enable) {
|
|
1046
|
+
wsEnableSkill(id);
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
wsDisableSkill(id);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}, [wsEnableSkill, wsDisableSkill]);
|
|
1053
|
+
const toggleMcpToolApproval = useCallback((serverId, toolName) => {
|
|
1054
|
+
setApprovedMcpTools(prev => {
|
|
1055
|
+
const newMap = new Map(prev);
|
|
1056
|
+
// Default: if no entry for this server, no tool is approved.
|
|
1057
|
+
const serverTools = new Set(prev.get(serverId) ?? []);
|
|
1058
|
+
const currentlyApproved = serverTools.has(toolName);
|
|
1059
|
+
if (currentlyApproved) {
|
|
1060
|
+
serverTools.delete(toolName);
|
|
336
1061
|
}
|
|
337
1062
|
else {
|
|
338
|
-
|
|
1063
|
+
serverTools.add(toolName);
|
|
1064
|
+
}
|
|
1065
|
+
newMap.set(serverId, serverTools);
|
|
1066
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1067
|
+
type: 'mcp_server_tool_approve',
|
|
1068
|
+
serverId,
|
|
1069
|
+
toolName,
|
|
1070
|
+
approved: !currentlyApproved,
|
|
1071
|
+
}, activeAgentId);
|
|
1072
|
+
if (!ok) {
|
|
1073
|
+
console.warn('[ChatBase] mcp_server_tool_approve dropped: websocket not ready');
|
|
339
1074
|
}
|
|
340
|
-
return
|
|
1075
|
+
return newMap;
|
|
341
1076
|
});
|
|
342
|
-
}, []);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
1077
|
+
}, [activeAgentId]);
|
|
1078
|
+
const toggleSkillApproval = useCallback((skillId) => {
|
|
1079
|
+
if (approvedSkills.has(skillId)) {
|
|
1080
|
+
setLocalSkillApproval(prev => {
|
|
1081
|
+
const next = new Map(prev);
|
|
1082
|
+
next.set(skillId, false);
|
|
1083
|
+
return next;
|
|
1084
|
+
});
|
|
1085
|
+
wsUnapproveSkill(skillId);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
setLocalSkillApproval(prev => {
|
|
1089
|
+
const next = new Map(prev);
|
|
1090
|
+
next.set(skillId, true);
|
|
1091
|
+
return next;
|
|
1092
|
+
});
|
|
1093
|
+
wsApproveSkill(skillId);
|
|
1094
|
+
}
|
|
1095
|
+
}, [approvedSkills, wsApproveSkill, wsUnapproveSkill]);
|
|
346
1096
|
const getEnabledMcpToolNames = useCallback(() => {
|
|
347
1097
|
const toolNames = [];
|
|
348
1098
|
enabledMcpTools.forEach((tools, serverId) => {
|
|
@@ -356,14 +1106,21 @@ pendingPrompt, }) {
|
|
|
356
1106
|
return Array.from(enabledSkills);
|
|
357
1107
|
}, [enabledSkills]);
|
|
358
1108
|
// ---- Load messages from store on mount ----
|
|
1109
|
+
// Only hydrate from the shared ``useChatStore`` when there is no
|
|
1110
|
+
// ``runtimeId`` (pure store mode without server-backed history). When a
|
|
1111
|
+
// ``runtimeId`` is provided the "Conversation history loading" effect
|
|
1112
|
+
// below is the single source of truth — reading from the shared store
|
|
1113
|
+
// here would otherwise leak messages from a previously-mounted
|
|
1114
|
+
// ``ChatBase`` (e.g. after switching examples) before the store reset
|
|
1115
|
+
// or history fetch completes.
|
|
359
1116
|
useEffect(() => {
|
|
360
|
-
if (useStoreMode) {
|
|
1117
|
+
if (useStoreMode && !runtimeId) {
|
|
361
1118
|
const storeMessages = useChatStore.getState().messages;
|
|
362
1119
|
if (storeMessages.length > 0) {
|
|
363
1120
|
setDisplayItems(storeMessages);
|
|
364
1121
|
}
|
|
365
1122
|
}
|
|
366
|
-
}, [useStoreMode]);
|
|
1123
|
+
}, [useStoreMode, runtimeId]);
|
|
367
1124
|
// ---- Conversation history loading ----
|
|
368
1125
|
const prevRuntimeIdRef = useRef(undefined);
|
|
369
1126
|
useEffect(() => {
|
|
@@ -374,11 +1131,14 @@ pendingPrompt, }) {
|
|
|
374
1131
|
if (!runtimeId)
|
|
375
1132
|
return;
|
|
376
1133
|
}
|
|
377
|
-
|
|
1134
|
+
if (!runtimeId)
|
|
378
1135
|
return;
|
|
379
|
-
}
|
|
380
1136
|
const store = useConversationStore.getState();
|
|
1137
|
+
const currentlyFetching = store.isFetching(runtimeId);
|
|
381
1138
|
if (!store.needsFetch(runtimeId)) {
|
|
1139
|
+
if (currentlyFetching) {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
382
1142
|
const storedMessages = store.getMessages(runtimeId);
|
|
383
1143
|
if (storedMessages.length > 0) {
|
|
384
1144
|
setDisplayItems(storedMessages);
|
|
@@ -387,83 +1147,54 @@ pendingPrompt, }) {
|
|
|
387
1147
|
return;
|
|
388
1148
|
}
|
|
389
1149
|
store.setFetching(runtimeId, true);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1150
|
+
const fullContextToMessages = () => extractChatMessagesFromFullContext(agentRuntimeStore.getState().fullContext);
|
|
1151
|
+
const applyMessages = (messages) => {
|
|
1152
|
+
if (messages.length > 0) {
|
|
1153
|
+
store.setMessages(runtimeId, messages);
|
|
1154
|
+
setDisplayItems(convertHistoryToDisplayItems(messages));
|
|
1155
|
+
}
|
|
394
1156
|
store.markFetched(runtimeId);
|
|
395
1157
|
setHistoryLoaded(true);
|
|
1158
|
+
};
|
|
1159
|
+
const existingMessages = fullContextToMessages();
|
|
1160
|
+
if (existingMessages.length > 0) {
|
|
1161
|
+
applyMessages(existingMessages);
|
|
396
1162
|
return;
|
|
397
1163
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1164
|
+
// Ask the monitoring websocket for a fresh snapshot and wait briefly
|
|
1165
|
+
// for `fullContext.messages` to arrive.
|
|
1166
|
+
const refreshRequested = agentRuntimeStore.getState().requestRefresh();
|
|
1167
|
+
if (!refreshRequested) {
|
|
1168
|
+
// Socket not ready yet; allow a later retry (e.g. when wsState changes).
|
|
1169
|
+
store.setFetching(runtimeId, false);
|
|
1170
|
+
setHistoryLoaded(true);
|
|
1171
|
+
return;
|
|
401
1172
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
'Content-Type': 'application/json',
|
|
407
|
-
};
|
|
408
|
-
if (authToken) {
|
|
409
|
-
headers['Authorization'] = `Bearer ${authToken}`;
|
|
410
|
-
}
|
|
411
|
-
const response = await fetch(endpoint, {
|
|
412
|
-
method: 'GET',
|
|
413
|
-
headers,
|
|
414
|
-
credentials: 'include',
|
|
415
|
-
});
|
|
416
|
-
if (!response.ok) {
|
|
417
|
-
throw new Error(`Failed to fetch history: ${response.status} ${response.statusText}`);
|
|
418
|
-
}
|
|
419
|
-
const data = await response.json();
|
|
420
|
-
const messages = (data.messages || []).map((msg) => {
|
|
421
|
-
if (msg.toolCalls && Array.isArray(msg.toolCalls)) {
|
|
422
|
-
msg.toolCalls = msg.toolCalls.map((tc) => {
|
|
423
|
-
if (tc.toolCallId && tc.toolName)
|
|
424
|
-
return tc;
|
|
425
|
-
let parsedArgs = tc.args ?? tc.arguments ?? {};
|
|
426
|
-
if (typeof parsedArgs === 'string') {
|
|
427
|
-
try {
|
|
428
|
-
parsedArgs = JSON.parse(parsedArgs);
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
parsedArgs = {};
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return {
|
|
435
|
-
type: 'tool-call',
|
|
436
|
-
toolCallId: tc.toolCallId ?? tc.id ?? tc.tool_call_id ?? '',
|
|
437
|
-
toolName: tc.toolName ?? tc.name ?? tc.tool_name ?? '',
|
|
438
|
-
args: parsedArgs,
|
|
439
|
-
status: tc.status ?? 'completed',
|
|
440
|
-
};
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
return msg;
|
|
444
|
-
});
|
|
445
|
-
if (messages.length > 0) {
|
|
446
|
-
store.setMessages(runtimeId, messages);
|
|
447
|
-
const items = convertHistoryToDisplayItems(messages);
|
|
448
|
-
setDisplayItems(items);
|
|
449
|
-
}
|
|
450
|
-
store.markFetched(runtimeId);
|
|
451
|
-
setHistoryLoaded(true);
|
|
1173
|
+
let resolved = false;
|
|
1174
|
+
const unsubscribe = agentRuntimeStore.subscribe(state => state.fullContext, nextFullContext => {
|
|
1175
|
+
if (resolved || !nextFullContext) {
|
|
1176
|
+
return;
|
|
452
1177
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1178
|
+
resolved = true;
|
|
1179
|
+
unsubscribe();
|
|
1180
|
+
const messages = extractChatMessagesFromFullContext(nextFullContext);
|
|
1181
|
+
applyMessages(messages);
|
|
1182
|
+
});
|
|
1183
|
+
const timeout = window.setTimeout(() => {
|
|
1184
|
+
if (resolved) {
|
|
1185
|
+
return;
|
|
457
1186
|
}
|
|
1187
|
+
resolved = true;
|
|
1188
|
+
unsubscribe();
|
|
1189
|
+
// Do not mark as fetched on timeout; keep it retryable for late WS snapshots.
|
|
1190
|
+
store.setFetching(runtimeId, false);
|
|
1191
|
+
setHistoryLoaded(true);
|
|
1192
|
+
}, 2000);
|
|
1193
|
+
return () => {
|
|
1194
|
+
window.clearTimeout(timeout);
|
|
1195
|
+
unsubscribe();
|
|
458
1196
|
};
|
|
459
|
-
|
|
460
|
-
}, [
|
|
461
|
-
runtimeId,
|
|
462
|
-
historyEndpoint,
|
|
463
|
-
historyAuthToken,
|
|
464
|
-
protocol?.endpoint,
|
|
465
|
-
protocol?.authToken,
|
|
466
|
-
]);
|
|
1197
|
+
}, [runtimeId, historyEndpoint, protocol?.agentId, wsState]);
|
|
467
1198
|
// Keep in-memory store in sync with displayItems
|
|
468
1199
|
useEffect(() => {
|
|
469
1200
|
if (runtimeId && displayItems.length > 0) {
|
|
@@ -483,7 +1214,7 @@ pendingPrompt, }) {
|
|
|
483
1214
|
prevMessageCountRef.current = currentCount;
|
|
484
1215
|
onMessagesChange?.(messages);
|
|
485
1216
|
}
|
|
486
|
-
}, [displayItems, onMessagesChange]);
|
|
1217
|
+
}, [displayItems, messages, onMessagesChange]);
|
|
487
1218
|
const padding = compact ? 2 : 3;
|
|
488
1219
|
// Derive approval config from protocol for built-in tool approval support
|
|
489
1220
|
const approvalConfig = useMemo(() => {
|
|
@@ -517,6 +1248,33 @@ pendingPrompt, }) {
|
|
|
517
1248
|
unsubscribeRef.current = adapter.subscribe((event) => {
|
|
518
1249
|
switch (event.type) {
|
|
519
1250
|
case 'message':
|
|
1251
|
+
if (event.usage) {
|
|
1252
|
+
const timestampMs = event.timestamp instanceof Date
|
|
1253
|
+
? event.timestamp.getTime()
|
|
1254
|
+
: Date.now();
|
|
1255
|
+
const promptTokens = Math.max(0, event.usage.promptTokens ?? 0);
|
|
1256
|
+
const completionTokens = Math.max(0, event.usage.completionTokens ?? 0);
|
|
1257
|
+
const totalTokens = Math.max(promptTokens + completionTokens, event.usage.totalTokens ?? 0);
|
|
1258
|
+
const runtimeState = agentRuntimeStore.getState();
|
|
1259
|
+
runtimeState.appendLocalTokenTurn({
|
|
1260
|
+
serviceName: monitoringServiceName,
|
|
1261
|
+
agentId: protocol?.agentId,
|
|
1262
|
+
timestampMs,
|
|
1263
|
+
promptTokens,
|
|
1264
|
+
completionTokens,
|
|
1265
|
+
totalTokens,
|
|
1266
|
+
});
|
|
1267
|
+
const liveCumulativeUsd = runtimeState.costUsage?.cumulativeCostUsd;
|
|
1268
|
+
if (typeof liveCumulativeUsd === 'number' &&
|
|
1269
|
+
Number.isFinite(liveCumulativeUsd)) {
|
|
1270
|
+
runtimeState.upsertLocalCostPoint({
|
|
1271
|
+
serviceName: monitoringServiceName,
|
|
1272
|
+
agentId: protocol?.agentId,
|
|
1273
|
+
timestampMs,
|
|
1274
|
+
cumulativeUsd: Math.max(0, liveCumulativeUsd),
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
520
1278
|
if (suppressAssistantTextForToolOnlyRef.current) {
|
|
521
1279
|
const suppressedMessageId = currentAssistantMessageRef.current?.id;
|
|
522
1280
|
if (suppressedMessageId) {
|
|
@@ -635,6 +1393,12 @@ pendingPrompt, }) {
|
|
|
635
1393
|
};
|
|
636
1394
|
toolCallsRef.current.set(toolCallId, toolCallMsg);
|
|
637
1395
|
setDisplayItems(prev => [...prev, toolCallMsg]);
|
|
1396
|
+
// Fire pre-hook for new tool calls
|
|
1397
|
+
onToolCallStartRef.current?.({
|
|
1398
|
+
toolName,
|
|
1399
|
+
toolCallId,
|
|
1400
|
+
args,
|
|
1401
|
+
});
|
|
638
1402
|
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
639
1403
|
const toolHandler = frontendTool?.handler;
|
|
640
1404
|
// Only execute when we have actual args. AG-UI emits an
|
|
@@ -714,6 +1478,15 @@ pendingPrompt, }) {
|
|
|
714
1478
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
715
1479
|
? updatedToolCall
|
|
716
1480
|
: item));
|
|
1481
|
+
// Fire post-hook for tool results
|
|
1482
|
+
onToolCallCompleteRef.current?.({
|
|
1483
|
+
toolName: existingToolCall.toolName,
|
|
1484
|
+
toolCallId,
|
|
1485
|
+
args: existingToolCall.args,
|
|
1486
|
+
result: event.toolResult.result,
|
|
1487
|
+
status: updatedToolCall.status,
|
|
1488
|
+
error: event.toolResult.error,
|
|
1489
|
+
});
|
|
717
1490
|
}
|
|
718
1491
|
}
|
|
719
1492
|
}
|
|
@@ -779,6 +1552,7 @@ pendingPrompt, }) {
|
|
|
779
1552
|
pendingToolExecutionsRef.current = 0;
|
|
780
1553
|
setIsLoading(false);
|
|
781
1554
|
setIsStreaming(false);
|
|
1555
|
+
agentRuntimeStore.getState().requestRefresh();
|
|
782
1556
|
break;
|
|
783
1557
|
case 'error':
|
|
784
1558
|
console.error('[ChatBase] Protocol error:', event.error);
|
|
@@ -813,6 +1587,7 @@ pendingPrompt, }) {
|
|
|
813
1587
|
pendingToolExecutionsRef.current = 0;
|
|
814
1588
|
setIsLoading(false);
|
|
815
1589
|
setIsStreaming(false);
|
|
1590
|
+
agentRuntimeStore.getState().requestRefresh();
|
|
816
1591
|
break;
|
|
817
1592
|
}
|
|
818
1593
|
});
|
|
@@ -976,7 +1751,7 @@ pendingPrompt, }) {
|
|
|
976
1751
|
description: tool.description,
|
|
977
1752
|
parameters: tool.parameters || { type: 'object', properties: {} },
|
|
978
1753
|
}));
|
|
979
|
-
console.
|
|
1754
|
+
console.warn('[ChatBase] frontendTools count:', frontendTools?.length ?? 0, 'toolsForRequest:', toolsForRequest.map(t => t.name));
|
|
980
1755
|
const enabledMcpToolNames = getEnabledMcpToolNames();
|
|
981
1756
|
const enabledSkillIds = getEnabledSkillIds();
|
|
982
1757
|
await adapterRef.current.sendMessage(userMessage, {
|
|
@@ -1007,6 +1782,7 @@ pendingPrompt, }) {
|
|
|
1007
1782
|
if (!adapterRef.current) {
|
|
1008
1783
|
setIsLoading(false);
|
|
1009
1784
|
setIsStreaming(false);
|
|
1785
|
+
agentRuntimeStore.getState().requestRefresh();
|
|
1010
1786
|
}
|
|
1011
1787
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
1012
1788
|
currentAssistantMessageRef.current = null;
|
|
@@ -1048,6 +1824,7 @@ pendingPrompt, }) {
|
|
|
1048
1824
|
adapterReady,
|
|
1049
1825
|
handleSend,
|
|
1050
1826
|
onSendMessage,
|
|
1827
|
+
applyServerApprovalDecision,
|
|
1051
1828
|
]);
|
|
1052
1829
|
// ---- handleStop ----
|
|
1053
1830
|
const handleStop = useCallback(() => {
|
|
@@ -1102,20 +1879,11 @@ pendingPrompt, }) {
|
|
|
1102
1879
|
pendingToolExecutionsRef.current = 0;
|
|
1103
1880
|
setIsLoading(false);
|
|
1104
1881
|
setIsStreaming(false);
|
|
1882
|
+
agentRuntimeStore.getState().requestRefresh();
|
|
1105
1883
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
1106
1884
|
currentAssistantMessageRef.current = null;
|
|
1107
1885
|
// Also interrupt any code running in the sandbox (best-effort).
|
|
1108
|
-
|
|
1109
|
-
const query = protocol.agentId
|
|
1110
|
-
? `?agent_id=${encodeURIComponent(protocol.agentId)}`
|
|
1111
|
-
: '';
|
|
1112
|
-
const interruptUrl = `${getApiBaseFromConfig(protocol.configEndpoint)}/configure/sandbox/interrupt${query}`;
|
|
1113
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
1114
|
-
if (protocol.authToken) {
|
|
1115
|
-
headers['Authorization'] = `Bearer ${protocol.authToken}`;
|
|
1116
|
-
}
|
|
1117
|
-
fetch(interruptUrl, { method: 'POST', headers }).catch(() => { });
|
|
1118
|
-
}
|
|
1886
|
+
sandboxStatusQuery.interrupt();
|
|
1119
1887
|
}, [
|
|
1120
1888
|
useStoreMode,
|
|
1121
1889
|
protocol?.configEndpoint,
|
|
@@ -1149,23 +1917,6 @@ pendingPrompt, }) {
|
|
|
1149
1917
|
headerButtons?.onClear?.();
|
|
1150
1918
|
}
|
|
1151
1919
|
}, [clearStoreMessages, onClear, headerButtons, useStoreMode, runtimeId]);
|
|
1152
|
-
// ---- handleSandboxInterrupt ----
|
|
1153
|
-
const handleSandboxInterrupt = useCallback(async () => {
|
|
1154
|
-
if (!protocol?.configEndpoint)
|
|
1155
|
-
return;
|
|
1156
|
-
const interruptUrl = `${getApiBaseFromConfig(protocol.configEndpoint)}/configure/sandbox/interrupt`;
|
|
1157
|
-
try {
|
|
1158
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
1159
|
-
if (protocol.authToken) {
|
|
1160
|
-
headers['Authorization'] = `Bearer ${protocol.authToken}`;
|
|
1161
|
-
}
|
|
1162
|
-
await fetch(interruptUrl, { method: 'POST', headers });
|
|
1163
|
-
sandboxStatusQuery.refetch();
|
|
1164
|
-
}
|
|
1165
|
-
catch {
|
|
1166
|
-
// Interrupt is best-effort
|
|
1167
|
-
}
|
|
1168
|
-
}, [protocol?.configEndpoint, protocol?.authToken, sandboxStatusQuery]);
|
|
1169
1920
|
// ---- HITL respond handler (passed to MessageList) ----
|
|
1170
1921
|
const handleRespond = useCallback(async (toolCallId, result) => {
|
|
1171
1922
|
const existingToolCall = toolCallsRef.current.get(toolCallId);
|
|
@@ -1174,28 +1925,85 @@ pendingPrompt, }) {
|
|
|
1174
1925
|
existingToolCall.status === 'inProgress')) {
|
|
1175
1926
|
const isApprovalDecision = !!result &&
|
|
1176
1927
|
typeof result === 'object' &&
|
|
1177
|
-
result.type ===
|
|
1928
|
+
result.type ===
|
|
1929
|
+
'tool-approval-decision' &&
|
|
1178
1930
|
typeof result.approved === 'boolean';
|
|
1179
1931
|
if (isApprovalDecision && adapterRef.current) {
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1932
|
+
const resultRecord = result;
|
|
1933
|
+
const approved = Boolean(resultRecord.approved);
|
|
1934
|
+
const fromServerEcho = resultRecord._fromServerEcho === true;
|
|
1935
|
+
const alreadyDispatched = resultRecord._alreadyDispatched === true;
|
|
1936
|
+
const requiresClientContinuation = existingToolCall.status === 'inProgress';
|
|
1937
|
+
const rawToolName = typeof resultRecord.toolName === 'string'
|
|
1938
|
+
? resultRecord.toolName
|
|
1939
|
+
: existingToolCall.toolName;
|
|
1940
|
+
const isMcpApprovalTool = rawToolName.includes('__');
|
|
1941
|
+
// When the user approves a tool call inline, immediately reflect that
|
|
1942
|
+
// approval in the tools dropdown so the toggle switches to "On".
|
|
1943
|
+
// Tool names follow the convention "serverId__toolName" (MCP) or are
|
|
1944
|
+
// bare skill names. We extract the server prefix for MCP tools.
|
|
1945
|
+
if (approved) {
|
|
1946
|
+
const sep = rawToolName.indexOf('__');
|
|
1947
|
+
if (sep !== -1) {
|
|
1948
|
+
const serverId = rawToolName.slice(0, sep);
|
|
1949
|
+
const toolName = rawToolName.slice(sep + 2);
|
|
1950
|
+
setApprovedMcpTools(prev => {
|
|
1951
|
+
const newMap = new Map(prev);
|
|
1952
|
+
const tools = new Set(prev.get(serverId) ?? []);
|
|
1953
|
+
tools.add(toolName);
|
|
1954
|
+
newMap.set(serverId, tools);
|
|
1955
|
+
return newMap;
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1193
1959
|
try {
|
|
1194
1960
|
const approvalId = typeof result === 'object' &&
|
|
1195
1961
|
result !== null &&
|
|
1196
1962
|
typeof result.approvalId === 'string'
|
|
1197
1963
|
? result.approvalId
|
|
1198
1964
|
: undefined;
|
|
1965
|
+
// Match AgentToolApprovalsExample semantics: first click sends only
|
|
1966
|
+
// a websocket decision; continuation waits for server echo.
|
|
1967
|
+
// Deferred pending-approval calls (`status === 'inProgress'`) still
|
|
1968
|
+
// require an explicit sendToolResult continuation from the client,
|
|
1969
|
+
// so do not return early in that mode.
|
|
1970
|
+
if (!fromServerEcho && !requiresClientContinuation) {
|
|
1971
|
+
if (approvalId) {
|
|
1972
|
+
if (approved) {
|
|
1973
|
+
await onApproveApproval?.(approvalId, undefined, toolCallId);
|
|
1974
|
+
}
|
|
1975
|
+
else {
|
|
1976
|
+
await onRejectApproval?.(approvalId, undefined, toolCallId);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (approvalId && !alreadyDispatched) {
|
|
1982
|
+
if (approved) {
|
|
1983
|
+
await onApproveApproval?.(approvalId, undefined, toolCallId);
|
|
1984
|
+
}
|
|
1985
|
+
else {
|
|
1986
|
+
await onRejectApproval?.(approvalId, undefined, toolCallId);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
// MCP approvals are unblocked server-side by the websocket decision
|
|
1990
|
+
// and continue on the same stream. Avoid sending an extra
|
|
1991
|
+
// sendToolResult continuation, which can trigger duplicate approvals.
|
|
1992
|
+
if (isMcpApprovalTool && !requiresClientContinuation) {
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const updatedToolCall = {
|
|
1996
|
+
...existingToolCall,
|
|
1997
|
+
result,
|
|
1998
|
+
status: approved ? 'complete' : 'error',
|
|
1999
|
+
error: approved ? undefined : 'Tool approval rejected by user',
|
|
2000
|
+
};
|
|
2001
|
+
toolCallsRef.current.set(toolCallId, updatedToolCall);
|
|
2002
|
+
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2003
|
+
? updatedToolCall
|
|
2004
|
+
: item));
|
|
2005
|
+
setIsLoading(true);
|
|
2006
|
+
setIsStreaming(true);
|
|
1199
2007
|
await adapterRef.current.sendToolResult(toolCallId, {
|
|
1200
2008
|
toolCallId,
|
|
1201
2009
|
success: approved,
|
|
@@ -1210,9 +2018,7 @@ pendingPrompt, }) {
|
|
|
1210
2018
|
message: 'Tool call rejected by user.',
|
|
1211
2019
|
...(approvalId ? { approvalId } : {}),
|
|
1212
2020
|
},
|
|
1213
|
-
...(approved
|
|
1214
|
-
? {}
|
|
1215
|
-
: { error: 'Tool approval rejected by user' }),
|
|
2021
|
+
...(approved ? {} : { error: 'Tool approval rejected by user' }),
|
|
1216
2022
|
});
|
|
1217
2023
|
}
|
|
1218
2024
|
catch (err) {
|
|
@@ -1275,7 +2081,8 @@ pendingPrompt, }) {
|
|
|
1275
2081
|
// event will handle it when the run truly completes.
|
|
1276
2082
|
}
|
|
1277
2083
|
}
|
|
1278
|
-
}, [displayItems]);
|
|
2084
|
+
}, [displayItems, onApproveApproval, onRejectApproval]);
|
|
2085
|
+
handleRespondRef.current = handleRespond;
|
|
1279
2086
|
// ---- Suggestion handlers (for EmptyState) ----
|
|
1280
2087
|
const handleSuggestionSubmit = useCallback((suggestion) => {
|
|
1281
2088
|
void handleSend(suggestion.message);
|
|
@@ -1284,6 +2091,78 @@ pendingPrompt, }) {
|
|
|
1284
2091
|
setInput(message);
|
|
1285
2092
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
1286
2093
|
}, []);
|
|
2094
|
+
// Banner approvals dispatch the decision immediately; the continuation is
|
|
2095
|
+
// resumed when the runtime websocket echoes approved/rejected.
|
|
2096
|
+
const handleBannerApprove = useCallback(async (approvalId, note) => {
|
|
2097
|
+
// Decision dispatch happens here; continuation is resumed on server echo.
|
|
2098
|
+
await onApproveApproval?.(approvalId, note);
|
|
2099
|
+
}, [onApproveApproval]);
|
|
2100
|
+
const handleBannerReject = useCallback(async (approvalId, note) => {
|
|
2101
|
+
// Decision dispatch happens here; continuation is resumed on server echo.
|
|
2102
|
+
await onRejectApproval?.(approvalId, note);
|
|
2103
|
+
}, [onRejectApproval]);
|
|
2104
|
+
// ---- Compute data for InputToolbar ----
|
|
2105
|
+
// Merge real-time WebSocket MCP status into the cached config data so the
|
|
2106
|
+
// dropdown reflects live availability even when the config query was cached
|
|
2107
|
+
// before the MCP servers finished starting.
|
|
2108
|
+
const configMcpServers = (configQuery.data?.mcpServers || []).filter(server => !mcpServers || isServerSelected(server));
|
|
2109
|
+
const filteredMcpServers = useMemo(() => {
|
|
2110
|
+
const merged = configMcpServers.map(server => {
|
|
2111
|
+
const wsServer = mcpStatusData?.servers?.find(s => s.id === server.id);
|
|
2112
|
+
if (wsServer && wsServer.status === 'started') {
|
|
2113
|
+
const updates = {};
|
|
2114
|
+
if (!server.isAvailable) {
|
|
2115
|
+
updates.isAvailable = true;
|
|
2116
|
+
}
|
|
2117
|
+
// Always prefer WS-discovered tools over cached config data.
|
|
2118
|
+
// The config query may have been fetched before MCP servers
|
|
2119
|
+
// finished starting, leaving tools empty or stale.
|
|
2120
|
+
if (wsServer.tools && wsServer.tools.length > 0) {
|
|
2121
|
+
updates.tools = wsServer.tools.map(t => ({
|
|
2122
|
+
name: t.name,
|
|
2123
|
+
description: t.description || '',
|
|
2124
|
+
enabled: t.enabled ?? true,
|
|
2125
|
+
}));
|
|
2126
|
+
}
|
|
2127
|
+
if (Object.keys(updates).length > 0) {
|
|
2128
|
+
return { ...server, ...updates };
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return server;
|
|
2132
|
+
});
|
|
2133
|
+
// Include WS-only servers that are started but missing from the config
|
|
2134
|
+
// query (e.g. config was fetched before the MCP server finished starting).
|
|
2135
|
+
const configIds = new Set(configMcpServers.map(s => s.id));
|
|
2136
|
+
for (const wsServer of mcpStatusData?.servers ?? []) {
|
|
2137
|
+
if (wsServer.status === 'started' &&
|
|
2138
|
+
!configIds.has(wsServer.id) &&
|
|
2139
|
+
wsServer.tools &&
|
|
2140
|
+
wsServer.tools.length > 0) {
|
|
2141
|
+
const selected = !mcpServers || mcpServers.some(s => s.id === wsServer.id);
|
|
2142
|
+
if (selected) {
|
|
2143
|
+
merged.push({
|
|
2144
|
+
id: wsServer.id,
|
|
2145
|
+
name: wsServer.id,
|
|
2146
|
+
description: '',
|
|
2147
|
+
url: '',
|
|
2148
|
+
enabled: true,
|
|
2149
|
+
tools: wsServer.tools.map(t => ({
|
|
2150
|
+
name: t.name,
|
|
2151
|
+
description: t.description || '',
|
|
2152
|
+
enabled: t.enabled ?? true,
|
|
2153
|
+
})),
|
|
2154
|
+
args: [],
|
|
2155
|
+
requiredEnvVars: [],
|
|
2156
|
+
isAvailable: true,
|
|
2157
|
+
transport: 'stdio',
|
|
2158
|
+
isConfig: false,
|
|
2159
|
+
isRunning: true,
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return merged;
|
|
2165
|
+
}, [configMcpServers, mcpStatusData, mcpServers]);
|
|
1287
2166
|
// ---- Not ready ----
|
|
1288
2167
|
if (!ready) {
|
|
1289
2168
|
return (_jsx(Box, { className: className, sx: {
|
|
@@ -1306,8 +2185,10 @@ pendingPrompt, }) {
|
|
|
1306
2185
|
const indicatorApiBase = protocol?.configEndpoint
|
|
1307
2186
|
? protocol.configEndpoint.replace(/\/api\/v1\/(config|configure)\/?$/, '')
|
|
1308
2187
|
: undefined;
|
|
1309
|
-
|
|
1310
|
-
|
|
2188
|
+
const connectionConfirmed = !protocol ||
|
|
2189
|
+
protocol.enableConfigQuery === false ||
|
|
2190
|
+
!!configQuery.data ||
|
|
2191
|
+
!!skillsQuery.data;
|
|
1311
2192
|
// ========================================================================
|
|
1312
2193
|
// Render
|
|
1313
2194
|
// ========================================================================
|
|
@@ -1320,7 +2201,9 @@ pendingPrompt, }) {
|
|
|
1320
2201
|
border,
|
|
1321
2202
|
boxShadow,
|
|
1322
2203
|
overflow: 'hidden',
|
|
1323
|
-
}, children: [showHeader && (_jsx(ChatBaseHeader, { title: title, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding,
|
|
2204
|
+
}, children: [showHeader && (_jsx(ChatBaseHeader, { title: title, subtitle: subtitle, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding, sandboxApiBase: indicatorApiBase, sandboxAuthToken: protocol?.authToken, sandboxAgentId: protocol?.agentId, sandboxStatusData: sandboxStatusData, headerButtons: headerButtons, messageCount: messages.length, onNewChat: handleNewChat, onClear: handleClear, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange })), showToolApprovalBanner &&
|
|
2205
|
+
pendingApprovals &&
|
|
2206
|
+
pendingApprovals.length > 0 && (_jsx(ToolApprovalBannerSection, { pendingApprovals: pendingApprovals, onApprove: handleBannerApprove, onReject: handleBannerReject })), showErrors && error && (_jsxs(Box, { sx: {
|
|
1324
2207
|
display: 'flex',
|
|
1325
2208
|
alignItems: 'center',
|
|
1326
2209
|
gap: 2,
|
|
@@ -1333,6 +2216,49 @@ pendingPrompt, }) {
|
|
|
1333
2216
|
flexDirection: 'column',
|
|
1334
2217
|
minHeight: '100%',
|
|
1335
2218
|
bg: 'canvas.default',
|
|
1336
|
-
}, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: renderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, emptyContent: _jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: description, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill }) }) })) }), footerContent, showInput && (_jsx(InputToolbar, { input: input, setInput: setInput, isLoading: isLoading, placeholder: placeholder, autoFocus: autoFocus, focusTrigger: focusTrigger, padding: padding, onSend: () => handleSend(), onStop: handleStop, showTokenUsage: showTokenUsage, agentUsage: agentUsage, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, codemodeEnabled: codemodeEnabled, isA2AProtocol: isA2AProtocol, hasConfigData: !!configQuery.data, hasSkillsData: !!skillsQuery.data, models: availableModels || configQuery.data?.models || [], selectedModel: selectedModel, onModelSelect: setSelectedModel, availableTools: configQuery.data?.builtinTools || [], mcpServers: filteredMcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: getEnabledMcpToolNames().length, onToggleMcpTool: toggleMcpTool, onToggleAllMcpServerTools: toggleAllMcpServerTools, skills: skillsQuery.data?.skills || [], skillsLoading: !!skillsQuery.isLoading, enabledSkills: enabledSkills, onToggleSkill: toggleSkill, onToggleAllSkills: toggleAllSkills, apiBase: indicatorApiBase, authToken: protocol?.authToken,
|
|
2219
|
+
}, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: renderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, emptyContent: _jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: description, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill }) }) })) }), footerContent, showInput && (_jsx(InputToolbar, { input: input, setInput: setInput, isLoading: isLoading, connectionConfirmed: connectionConfirmed, placeholder: placeholder, autoFocus: autoFocus, focusTrigger: focusTrigger, padding: padding, onSend: () => handleSend(), onStop: handleStop, disableInputPrompt: disableInputPrompt, showTokenUsage: showTokenUsage, agentUsage: agentUsage, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, isA2AProtocol: isA2AProtocol, hasConfigData: !!configQuery.data, hasSkillsData: !!skillsQuery.data, models: availableModels || configQuery.data?.models || [], selectedModel: selectedModel, onModelSelect: setSelectedModel, availableTools: configQuery.data?.builtinTools || [], mcpServers: filteredMcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: getEnabledMcpToolNames().length, onToggleMcpTool: toggleMcpTool, onToggleAllMcpServerTools: toggleAllMcpServerTools, approvedMcpTools: approvedMcpTools, onToggleMcpToolApproval: toggleMcpToolApproval, skills: skillsQuery.data?.skills || [], skillsLoading: !!skillsQuery.isLoading, enabledSkills: enabledSkills, onToggleSkill: toggleSkill, onToggleAllSkills: toggleAllSkills, approvedSkills: approvedSkills, onToggleSkillApproval: toggleSkillApproval, apiBase: indicatorApiBase, authToken: protocol?.authToken, mcpStatusData: mcpStatusData })), showPoweredBy && _jsx(PoweredByTag, { ...poweredByProps })] }));
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Internal component rendering the top-of-chat approval banner + review dialog.
|
|
2223
|
+
* Extracted so we can keep `ChatBase` focused on chat flow while still owning
|
|
2224
|
+
* the banner UX via the `showToolApprovalBanner` prop.
|
|
2225
|
+
*/
|
|
2226
|
+
function ToolApprovalBannerSection({ pendingApprovals, onApprove, onReject, }) {
|
|
2227
|
+
const [activeApproval, setActiveApproval] = useState(null);
|
|
2228
|
+
// Keep the active approval in sync with the incoming list; if the active
|
|
2229
|
+
// one is no longer pending (resolved), dismiss the dialog.
|
|
2230
|
+
useEffect(() => {
|
|
2231
|
+
if (!activeApproval) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (!pendingApprovals.some(a => a.id === activeApproval.id)) {
|
|
2235
|
+
setActiveApproval(null);
|
|
2236
|
+
}
|
|
2237
|
+
}, [pendingApprovals, activeApproval]);
|
|
2238
|
+
return (_jsxs(_Fragment, { children: [_jsx(ToolApprovalBanner, { pendingApprovals: pendingApprovals, onReview: approval => setActiveApproval(approval), onApproveAll: async () => {
|
|
2239
|
+
if (!onApprove)
|
|
2240
|
+
return;
|
|
2241
|
+
for (const approval of pendingApprovals) {
|
|
2242
|
+
await onApprove(approval.id);
|
|
2243
|
+
}
|
|
2244
|
+
} }), _jsx(ToolApprovalDialog, { isOpen: !!activeApproval, toolName: activeApproval?.toolName ?? '', toolDescription: activeApproval?.toolDescription, args: activeApproval?.args ?? {}, onApprove: async () => {
|
|
2245
|
+
if (!activeApproval || !onApprove) {
|
|
2246
|
+
setActiveApproval(null);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const result = await onApprove(activeApproval.id);
|
|
2250
|
+
if (result !== false) {
|
|
2251
|
+
setActiveApproval(null);
|
|
2252
|
+
}
|
|
2253
|
+
}, onDeny: async () => {
|
|
2254
|
+
if (!activeApproval || !onReject) {
|
|
2255
|
+
setActiveApproval(null);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const result = await onReject(activeApproval.id, 'Rejected from tool approval dialog');
|
|
2259
|
+
if (result !== false) {
|
|
2260
|
+
setActiveApproval(null);
|
|
2261
|
+
}
|
|
2262
|
+
}, onClose: () => setActiveApproval(null) })] }));
|
|
1337
2263
|
}
|
|
1338
2264
|
export default ChatBase;
|