@datalayer/agent-runtimes 1.0.4 → 1.0.6
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 +182 -1
- package/lib/AgentNode.d.ts +3 -0
- package/lib/AgentNode.js +676 -0
- package/lib/App.js +1 -1
- package/lib/agent-node/themeStore.d.ts +3 -0
- package/lib/agent-node/themeStore.js +156 -0
- package/lib/agent-node-main.d.ts +1 -0
- package/lib/agent-node-main.js +14 -0
- 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 +20 -14
- package/lib/chat/ChatFloating.d.ts +7 -140
- package/lib/chat/ChatFloating.js +3 -3
- 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 +83 -51
- package/lib/chat/ChatStandalone.d.ts +4 -54
- package/lib/chat/ChatStandalone.js +3 -3
- package/lib/chat/base/ChatBase.js +1414 -174
- package/lib/chat/display/FloatingBrandButton.js +8 -1
- package/lib/chat/header/ChatHeader.d.ts +3 -1
- package/lib/chat/header/ChatHeader.js +15 -12
- package/lib/chat/header/ChatHeaderBase.d.ts +30 -5
- package/lib/chat/header/ChatHeaderBase.js +41 -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 +91 -56
- 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 +154 -114
- package/lib/chat/messages/ChatMessages.js +6 -2
- package/lib/chat/prompt/InputFooter.d.ts +21 -6
- package/lib/chat/prompt/InputFooter.js +76 -20
- package/lib/chat/prompt/InputPrompt.d.ts +5 -1
- package/lib/chat/prompt/InputPrompt.js +4 -4
- package/lib/chat/prompt/InputPromptFooter.d.ts +3 -1
- package/lib/chat/prompt/InputPromptFooter.js +3 -3
- 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 +20 -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.d.ts +22 -0
- package/lib/config/AgentConfiguration.js +319 -64
- 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/AgUiSharedStateExample.js +2 -1
- package/lib/examples/AgentCheckpointsExample.js +14 -28
- package/lib/examples/AgentCodemodeExample.d.ts +4 -6
- package/lib/examples/AgentCodemodeExample.js +603 -169
- package/lib/examples/AgentEvalsExample.js +339 -53
- package/lib/examples/AgentGuardrailsExample.js +383 -66
- package/lib/examples/AgentHooksExample.d.ts +3 -0
- package/lib/examples/AgentHooksExample.js +122 -0
- package/lib/examples/AgentInferenceProviderExample.d.ts +3 -0
- package/lib/examples/AgentInferenceProviderExample.js +329 -0
- package/lib/examples/AgentMCPExample.d.ts +3 -0
- package/lib/examples/AgentMCPExample.js +481 -0
- package/lib/examples/AgentMemoryExample.d.ts +1 -2
- package/lib/examples/AgentMemoryExample.js +78 -33
- package/lib/examples/AgentMonitoringExample.js +261 -200
- package/lib/examples/AgentNotificationsExample.d.ts +1 -2
- package/lib/examples/AgentNotificationsExample.js +114 -33
- package/lib/examples/AgentOtelExample.js +32 -42
- package/lib/examples/AgentOutputsExample.d.ts +11 -6
- package/lib/examples/AgentOutputsExample.js +433 -81
- package/lib/examples/AgentParametersExample.d.ts +3 -0
- package/lib/examples/AgentParametersExample.js +248 -0
- package/lib/examples/AgentSandboxExample.d.ts +3 -3
- package/lib/examples/AgentSandboxExample.js +74 -45
- package/lib/examples/AgentSkillsExample.js +95 -103
- package/lib/examples/AgentSubagentsExample.d.ts +14 -0
- package/lib/examples/AgentSubagentsExample.js +228 -0
- package/lib/examples/AgentToolApprovalsExample.js +49 -561
- package/lib/examples/AgentTriggersExample.js +823 -569
- package/lib/examples/{AgentspecExample.d.ts → AgentspecsExample.d.ts} +2 -2
- package/lib/examples/AgentspecsExample.js +1096 -0
- package/lib/examples/ChatCustomExample.js +16 -28
- package/lib/examples/ChatExample.js +13 -29
- 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} +66 -17
- 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 +9 -3
- package/lib/examples/components/ExampleWrapper.js +45 -9
- 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 +108 -41
- package/lib/examples/index.d.ts +10 -6
- package/lib/examples/index.js +10 -6
- package/lib/examples/lexical/initial-content.json +6 -6
- package/lib/examples/main.js +257 -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/examples/utils/useExampleAgentRuntimesUrl.d.ts +5 -0
- package/lib/examples/utils/useExampleAgentRuntimesUrl.js +19 -0
- 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 +153 -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} +70 -4
- package/lib/hooks/{useAgents.js → useAgentRuntimes.js} +237 -32
- 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 +21 -4
- package/lib/specs/agents/agents.js +2879 -316
- package/lib/specs/agents/index.js +3 -1
- package/lib/specs/benchmarks.d.ts +20 -0
- package/lib/specs/benchmarks.js +205 -0
- package/lib/specs/envvars.js +27 -20
- package/lib/specs/evals.d.ts +10 -9
- package/lib/specs/evals.js +128 -88
- 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/mcpServers.js +35 -6
- package/lib/specs/memory.d.ts +0 -2
- package/lib/specs/memory.js +4 -17
- package/lib/specs/models.d.ts +0 -2
- package/lib/specs/models.js +20 -15
- 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 +1 -1
- package/lib/specs/skills.js +23 -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 +208 -0
- package/lib/stores/agentRuntimeStore.js +650 -0
- package/lib/stores/conversationStore.js +2 -2
- 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/agents-lifecycle.d.ts +18 -0
- package/lib/types/agents.d.ts +6 -0
- package/lib/types/agentspecs.d.ts +54 -1
- package/lib/types/benchmarks.d.ts +43 -0
- package/lib/types/benchmarks.js +5 -0
- package/lib/types/chat.d.ts +325 -8
- package/lib/types/context.d.ts +27 -0
- package/lib/types/cost.d.ts +2 -2
- package/lib/types/evals.d.ts +26 -17
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -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 +19 -11
- package/scripts/codegen/__pycache__/generate_agents.cpython-313.pyc +0 -0
- package/scripts/codegen/__pycache__/generate_benchmarks.cpython-313.pyc +0 -0
- package/scripts/codegen/__pycache__/generate_evals.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 +187 -45
- package/scripts/codegen/generate_benchmarks.py +441 -0
- package/scripts/codegen/generate_evals.py +94 -16
- package/scripts/codegen/generate_events.py +35 -14
- 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/AgentspecExample.js +0 -705
- 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/{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
|
@@ -18,23 +18,157 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
18
18
|
import { useContext } from 'react';
|
|
19
19
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
20
20
|
import { Text, Spinner } from '@primer/react';
|
|
21
|
-
import { Box, setupPrimerPortals } from '@datalayer/primer-addons';
|
|
21
|
+
import { Box, setupPrimerPortals, useThemeStore, getColorPalette, } 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 isDevTraceEnabled = () => {
|
|
45
|
+
try {
|
|
46
|
+
return Boolean(import.meta.env?.DEV);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const logApprovalTrace = (label, details) => {
|
|
53
|
+
if (!isDevTraceEnabled()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.debug(`[approval-trace] ${label}`, details);
|
|
57
|
+
};
|
|
58
|
+
const normalizeAgentId = (value) => (value ?? '').trim().toLowerCase();
|
|
59
|
+
const normalizeToolName = (value) => value.replace(/[-_]/g, '').toLowerCase();
|
|
60
|
+
const normalizeSkillApprovalId = (value) => {
|
|
61
|
+
const idx = value.indexOf(':');
|
|
62
|
+
if (idx <= 0)
|
|
63
|
+
return value;
|
|
64
|
+
return value.slice(0, idx);
|
|
65
|
+
};
|
|
66
|
+
const stableStringify = (value) => {
|
|
67
|
+
if (value === null || typeof value !== 'object') {
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
|
72
|
+
}
|
|
73
|
+
const entries = Object.entries(value)
|
|
74
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
75
|
+
.map(([key, itemValue]) => `${JSON.stringify(key)}:${stableStringify(itemValue)}`);
|
|
76
|
+
return `{${entries.join(',')}}`;
|
|
77
|
+
};
|
|
78
|
+
const approvalSignature = (toolName, args) => `${normalizeToolName(toolName)}::${stableStringify(args ?? {})}`;
|
|
79
|
+
const normalizeAiAgentsBaseUrl = (rawBaseUrl) => {
|
|
80
|
+
const trimmed = rawBaseUrl.replace(/\/$/, '');
|
|
81
|
+
if (trimmed.endsWith(AI_AGENTS_API_PREFIX)) {
|
|
82
|
+
return trimmed.slice(0, -AI_AGENTS_API_PREFIX.length);
|
|
83
|
+
}
|
|
84
|
+
return trimmed;
|
|
85
|
+
};
|
|
86
|
+
const toWsUrl = (baseUrl, path, token) => {
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(baseUrl);
|
|
89
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
90
|
+
url.pathname = path;
|
|
91
|
+
if (token) {
|
|
92
|
+
url.searchParams.set('token', token);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
url.search = '';
|
|
96
|
+
}
|
|
97
|
+
return url.toString();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const normalizeApprovalPayload = (input) => {
|
|
104
|
+
const id = (typeof input.id === 'string' && input.id) ||
|
|
105
|
+
(typeof input.approval_id === 'string' && input.approval_id) ||
|
|
106
|
+
(typeof input.approvalId === 'string' && input.approvalId) ||
|
|
107
|
+
'';
|
|
108
|
+
const toolName = (typeof input.tool_name === 'string' && input.tool_name) ||
|
|
109
|
+
(typeof input.toolName === 'string' && input.toolName) ||
|
|
110
|
+
'';
|
|
111
|
+
if (!id || !toolName) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const toolArgs = (input.tool_args && typeof input.tool_args === 'object'
|
|
115
|
+
? input.tool_args
|
|
116
|
+
: input.toolArgs && typeof input.toolArgs === 'object'
|
|
117
|
+
? input.toolArgs
|
|
118
|
+
: undefined) ?? {};
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
tool_name: toolName,
|
|
122
|
+
tool_args: toolArgs,
|
|
123
|
+
tool_call_id: (typeof input.tool_call_id === 'string' && input.tool_call_id) ||
|
|
124
|
+
(typeof input.toolCallId === 'string' && input.toolCallId) ||
|
|
125
|
+
undefined,
|
|
126
|
+
note: (typeof input.note === 'string' && input.note) ||
|
|
127
|
+
(typeof input.message === 'string' && input.message) ||
|
|
128
|
+
undefined,
|
|
129
|
+
status: (typeof input.status === 'string' && input.status) || 'pending',
|
|
130
|
+
agent_id: (typeof input.agent_id === 'string' && input.agent_id) ||
|
|
131
|
+
(typeof input.agentId === 'string' && input.agentId) ||
|
|
132
|
+
undefined,
|
|
133
|
+
created_at: (typeof input.created_at === 'string' && input.created_at) ||
|
|
134
|
+
(typeof input.createdAt === 'string' && input.createdAt) ||
|
|
135
|
+
undefined,
|
|
136
|
+
updated_at: (typeof input.updated_at === 'string' && input.updated_at) ||
|
|
137
|
+
(typeof input.updatedAt === 'string' && input.updatedAt) ||
|
|
138
|
+
undefined,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const statusFromApprovalEvent = (eventName) => {
|
|
142
|
+
if (eventName === 'tool_approval_created') {
|
|
143
|
+
return 'pending';
|
|
144
|
+
}
|
|
145
|
+
if (eventName === 'tool_approval_approved') {
|
|
146
|
+
return 'approved';
|
|
147
|
+
}
|
|
148
|
+
if (eventName === 'tool_approval_rejected') {
|
|
149
|
+
return 'rejected';
|
|
150
|
+
}
|
|
151
|
+
if (eventName === 'tool_approval_deleted') {
|
|
152
|
+
return 'deleted';
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
};
|
|
156
|
+
const RESOLVED_TOOL_CALL_SUPPRESSION_MS = 15_000;
|
|
157
|
+
const isApprovalForAgent = (approval, activeAgentId) => {
|
|
158
|
+
if (!activeAgentId) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (!approval.agent_id) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (approval.agent_id === activeAgentId) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const normalizedApprovalAgentId = normalizeAgentId(approval.agent_id);
|
|
168
|
+
const normalizedActiveAgentId = normalizeAgentId(activeAgentId);
|
|
169
|
+
return (normalizedApprovalAgentId.includes(normalizedActiveAgentId) ||
|
|
170
|
+
normalizedActiveAgentId.includes(normalizedApprovalAgentId));
|
|
171
|
+
};
|
|
38
172
|
function isToolCallOnlyPrompt(content) {
|
|
39
173
|
const normalized = content.toLowerCase();
|
|
40
174
|
return (/tool\s*call\s*only/.test(normalized) ||
|
|
@@ -59,6 +193,87 @@ function formatToolResultFallback(result) {
|
|
|
59
193
|
return 'Tool completed successfully.';
|
|
60
194
|
}
|
|
61
195
|
}
|
|
196
|
+
function extractChatMessagesFromFullContext(fullContext) {
|
|
197
|
+
if (!fullContext) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const rawMessages = Array.isArray(fullContext.messages)
|
|
201
|
+
? fullContext.messages
|
|
202
|
+
: [];
|
|
203
|
+
return rawMessages
|
|
204
|
+
.map((msg, index) => {
|
|
205
|
+
const role = String(msg.role || '').toLowerCase();
|
|
206
|
+
// Only hydrate conversational turns in the visible history.
|
|
207
|
+
// System/tool messages may contain internal prompts and metadata.
|
|
208
|
+
if (role !== 'user' && role !== 'assistant') {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const timestampValue = typeof msg.timestamp === 'string' && msg.timestamp.length > 0
|
|
212
|
+
? msg.timestamp
|
|
213
|
+
: new Date().toISOString();
|
|
214
|
+
const createdAt = new Date(timestampValue);
|
|
215
|
+
const content = typeof msg.content === 'string'
|
|
216
|
+
? msg.content
|
|
217
|
+
: JSON.stringify(msg.content ?? '');
|
|
218
|
+
return {
|
|
219
|
+
id: `history-${role}-${index}-${timestampValue}`,
|
|
220
|
+
role,
|
|
221
|
+
content,
|
|
222
|
+
createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,
|
|
223
|
+
};
|
|
224
|
+
})
|
|
225
|
+
.filter((m) => m !== null);
|
|
226
|
+
}
|
|
227
|
+
function parseEnabledMcpToolsByServer(mcpStatusData) {
|
|
228
|
+
if (!mcpStatusData || typeof mcpStatusData !== 'object') {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const raw = mcpStatusData.enabled_tools_by_server;
|
|
232
|
+
if (raw == null) {
|
|
233
|
+
return new Map();
|
|
234
|
+
}
|
|
235
|
+
if (typeof raw !== 'object') {
|
|
236
|
+
return new Map();
|
|
237
|
+
}
|
|
238
|
+
const parsed = new Map();
|
|
239
|
+
for (const [serverId, toolNames] of Object.entries(raw)) {
|
|
240
|
+
if (!Array.isArray(toolNames)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const validToolNames = toolNames.filter((name) => typeof name === 'string' && name.length > 0);
|
|
244
|
+
const normalizedToolNames = validToolNames.map(name => {
|
|
245
|
+
const sep = name.indexOf('__');
|
|
246
|
+
return sep >= 0 ? name.slice(sep + 2) : name;
|
|
247
|
+
});
|
|
248
|
+
parsed.set(serverId, new Set(normalizedToolNames));
|
|
249
|
+
}
|
|
250
|
+
return parsed;
|
|
251
|
+
}
|
|
252
|
+
function parseApprovedMcpToolsByServer(mcpStatusData) {
|
|
253
|
+
if (!mcpStatusData || typeof mcpStatusData !== 'object') {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const raw = mcpStatusData.approved_tools_by_server;
|
|
257
|
+
if (raw == null) {
|
|
258
|
+
return new Map();
|
|
259
|
+
}
|
|
260
|
+
if (typeof raw !== 'object') {
|
|
261
|
+
return new Map();
|
|
262
|
+
}
|
|
263
|
+
const parsed = new Map();
|
|
264
|
+
for (const [serverId, toolNames] of Object.entries(raw)) {
|
|
265
|
+
if (!Array.isArray(toolNames)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const validToolNames = toolNames.filter((name) => typeof name === 'string' && name.length > 0);
|
|
269
|
+
const normalizedToolNames = validToolNames.map(name => {
|
|
270
|
+
const sep = name.indexOf('__');
|
|
271
|
+
return sep >= 0 ? name.slice(sep + 2) : name;
|
|
272
|
+
});
|
|
273
|
+
parsed.set(serverId, new Set(normalizedToolNames));
|
|
274
|
+
}
|
|
275
|
+
return parsed;
|
|
276
|
+
}
|
|
62
277
|
// ---------------------------------------------------------------------------
|
|
63
278
|
// ChatBase (outer wrapper — ensures QueryClient is available)
|
|
64
279
|
// ---------------------------------------------------------------------------
|
|
@@ -100,26 +315,429 @@ export function ChatBase(props) {
|
|
|
100
315
|
// ---------------------------------------------------------------------------
|
|
101
316
|
// ChatBaseInner — contains all actual logic
|
|
102
317
|
// ---------------------------------------------------------------------------
|
|
103
|
-
function ChatBaseInner({ title, showHeader = false, showTokenUsage = true, showLoadingIndicator = true, showErrors = true, showInput = true, showModelSelector =
|
|
318
|
+
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, kernelIndicatorState, kernel, kernelEnvironmentName, kernelCpu, kernelMemory, kernelGpu, chatViewMode, onChatViewModeChange,
|
|
104
319
|
// Mode selection
|
|
105
320
|
useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, enableStreaming = false,
|
|
106
321
|
// Extended props
|
|
107
322
|
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,
|
|
323
|
+
// Tool invocation hooks
|
|
324
|
+
onToolCallStart, onToolCallComplete,
|
|
108
325
|
// Identity/Authorization props
|
|
109
|
-
onAuthorizationRequired, connectedIdentities,
|
|
326
|
+
onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
|
|
110
327
|
// Conversation persistence
|
|
111
|
-
runtimeId, historyEndpoint, historyAuthToken,
|
|
328
|
+
runtimeId, historyEndpoint, historyAuthToken: _historyAuthToken,
|
|
112
329
|
// Pending prompt
|
|
113
|
-
pendingPrompt,
|
|
330
|
+
pendingPrompt, contextSnapshot: externalContextSnapshot, mcpStatusData, sandboxStatusData,
|
|
331
|
+
// Tool approval banner
|
|
332
|
+
showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApproveApproval: onApproveApprovalProp, onRejectApproval: onRejectApprovalProp, }) {
|
|
114
333
|
useEffect(() => {
|
|
115
334
|
setupPrimerPortals();
|
|
116
335
|
}, []);
|
|
336
|
+
const { theme } = useThemeStore();
|
|
337
|
+
const assistantIconColor = getColorPalette(theme, 'dark').textLight;
|
|
338
|
+
// ── Built-in pending approvals from the agent-runtime Zustand store ──
|
|
339
|
+
// When the parent doesn't supply the `pendingApprovals` prop, derive them
|
|
340
|
+
// from the shared store so the banner works out-of-the-box.
|
|
341
|
+
const storeApprovals = useAgentRuntimeStore(s => s.approvals);
|
|
342
|
+
const storeMcpStatus = useAgentRuntimeStore(s => s.mcpStatus);
|
|
343
|
+
const effectiveMcpStatusData = mcpStatusData ?? storeMcpStatus;
|
|
344
|
+
const protocolConfig = typeof protocolRaw === 'object'
|
|
345
|
+
? protocolRaw
|
|
346
|
+
: undefined;
|
|
347
|
+
const configuredAiAgentsBaseUrl = useCoreStore((s) => s.configuration?.aiagentsRunUrl);
|
|
348
|
+
const activeAgentId = protocolConfig?.agentId || runtimeId;
|
|
349
|
+
const historyScopeId = runtimeId || activeAgentId;
|
|
350
|
+
const aiAgentsAuthToken = protocolConfig?.authToken;
|
|
351
|
+
const aiAgentsBaseUrl = useMemo(() => normalizeAiAgentsBaseUrl(configuredAiAgentsBaseUrl || DEFAULT_SERVICE_URLS.AI_AGENTS), [configuredAiAgentsBaseUrl]);
|
|
352
|
+
const aiAgentsApprovalWsRef = useRef(null);
|
|
353
|
+
const resolvedToolCallSuppressionsRef = useRef(new Map());
|
|
354
|
+
const sendAiAgentsApprovalDecision = useCallback((approvalId, approved, note) => {
|
|
355
|
+
const ws = aiAgentsApprovalWsRef.current;
|
|
356
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
357
|
+
logApprovalTrace('send_decision_skipped_ws_not_ready', {
|
|
358
|
+
approvalId,
|
|
359
|
+
approved,
|
|
360
|
+
wsReadyState: ws?.readyState,
|
|
361
|
+
});
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
logApprovalTrace('send_decision', {
|
|
366
|
+
approvalId,
|
|
367
|
+
approved,
|
|
368
|
+
hasNote: Boolean(note),
|
|
369
|
+
});
|
|
370
|
+
ws.send(JSON.stringify({
|
|
371
|
+
type: 'tool_approval_decision',
|
|
372
|
+
approvalId,
|
|
373
|
+
approved,
|
|
374
|
+
...(note ? { note } : {}),
|
|
375
|
+
}));
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
logApprovalTrace('send_decision_failed', {
|
|
380
|
+
approvalId,
|
|
381
|
+
approved,
|
|
382
|
+
});
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}, []);
|
|
386
|
+
const requestAiAgentsApprovalHistory = useCallback(() => {
|
|
387
|
+
const ws = aiAgentsApprovalWsRef.current;
|
|
388
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
ws.send(JSON.stringify({ type: 'tool-approvals-history' }));
|
|
393
|
+
logApprovalTrace('request_history', {});
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}, []);
|
|
400
|
+
const rememberResolvedToolCall = useCallback((toolCallId) => {
|
|
401
|
+
if (!toolCallId) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
resolvedToolCallSuppressionsRef.current.set(toolCallId, Date.now() + RESOLVED_TOOL_CALL_SUPPRESSION_MS);
|
|
405
|
+
logApprovalTrace('suppress_pending_for_tool_call', {
|
|
406
|
+
toolCallId,
|
|
407
|
+
windowMs: RESOLVED_TOOL_CALL_SUPPRESSION_MS,
|
|
408
|
+
});
|
|
409
|
+
}, []);
|
|
410
|
+
const isSuppressedPending = useCallback((toolCallId) => {
|
|
411
|
+
if (!toolCallId) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const expiresAt = resolvedToolCallSuppressionsRef.current.get(toolCallId);
|
|
415
|
+
if (!expiresAt) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
if (expiresAt < Date.now()) {
|
|
419
|
+
resolvedToolCallSuppressionsRef.current.delete(toolCallId);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}, []);
|
|
424
|
+
const queueApprovalDecisionRetry = useCallback((approvalId, approved, note) => {
|
|
425
|
+
window.setTimeout(() => {
|
|
426
|
+
const stillPending = agentRuntimeStore
|
|
427
|
+
.getState()
|
|
428
|
+
.approvals.some(approval => approval.id === approvalId && approval.status === 'pending');
|
|
429
|
+
if (!stillPending) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const resent = sendAiAgentsApprovalDecision(approvalId, approved, note);
|
|
433
|
+
logApprovalTrace('retry_send_decision', {
|
|
434
|
+
approvalId,
|
|
435
|
+
approved,
|
|
436
|
+
sent: resent,
|
|
437
|
+
});
|
|
438
|
+
if (resent) {
|
|
439
|
+
requestAiAgentsApprovalHistory();
|
|
440
|
+
}
|
|
441
|
+
}, 500);
|
|
442
|
+
}, [requestAiAgentsApprovalHistory, sendAiAgentsApprovalDecision]);
|
|
443
|
+
const storePendingApprovals = useMemo(() => {
|
|
444
|
+
if (pendingApprovalsProp)
|
|
445
|
+
return pendingApprovalsProp;
|
|
446
|
+
return storeApprovals
|
|
447
|
+
.filter(a => a.status === 'pending' && isApprovalForAgent(a, activeAgentId))
|
|
448
|
+
.map(a => ({
|
|
449
|
+
id: a.id,
|
|
450
|
+
toolName: a.tool_name,
|
|
451
|
+
toolDescription: a.note ?? undefined,
|
|
452
|
+
args: a.tool_args ?? {},
|
|
453
|
+
agentId: a.agent_id ?? '',
|
|
454
|
+
requestedAt: a.created_at ?? new Date().toISOString(),
|
|
455
|
+
}));
|
|
456
|
+
}, [pendingApprovalsProp, storeApprovals, activeAgentId]);
|
|
457
|
+
const pendingApprovals = storePendingApprovals;
|
|
458
|
+
// Persist a one-off approval decision into the tools/skills dropdown state
|
|
459
|
+
// on the agent-runtime so the "Approved" toggle moves Off→On across
|
|
460
|
+
// sessions and reloads. For MCP tool approvals (``<server>__<tool>``) this
|
|
461
|
+
// sends ``mcp_server_tool_approve``. For skill approvals
|
|
462
|
+
// (``skill:<skill_id>``) this sends ``skill_approve`` /
|
|
463
|
+
// ``skill_unapprove``.
|
|
464
|
+
const persistApprovalDecision = useCallback((approvalId, approved) => {
|
|
465
|
+
const approval = agentRuntimeStore
|
|
466
|
+
.getState()
|
|
467
|
+
.approvals.find(a => a.id === approvalId);
|
|
468
|
+
const toolName = approval?.tool_name;
|
|
469
|
+
if (!toolName)
|
|
470
|
+
return;
|
|
471
|
+
// Derive the skill id either from a synthetic ``skill:<id>`` tool_name
|
|
472
|
+
// OR from a skill-tool call (``run_skill_script`` / ``load_skill`` /
|
|
473
|
+
// ``read_skill_resource``) carrying ``skill`` / ``skill_name`` / ``name``
|
|
474
|
+
// in its args. This belt-and-suspenders approach makes sure the
|
|
475
|
+
// Approved toggle flips even if the server emits a bare approval
|
|
476
|
+
// request without the ``skill:`` prefix.
|
|
477
|
+
const deriveSkillId = () => {
|
|
478
|
+
if (toolName.startsWith('skill:')) {
|
|
479
|
+
const skillRef = toolName.slice('skill:'.length);
|
|
480
|
+
if (!skillRef)
|
|
481
|
+
return null;
|
|
482
|
+
return normalizeSkillApprovalId(skillRef);
|
|
483
|
+
}
|
|
484
|
+
const SKILL_TOOLS = new Set([
|
|
485
|
+
'run_skill_script',
|
|
486
|
+
'load_skill',
|
|
487
|
+
'read_skill_resource',
|
|
488
|
+
]);
|
|
489
|
+
if (!SKILL_TOOLS.has(toolName))
|
|
490
|
+
return null;
|
|
491
|
+
const a = (approval?.tool_args ?? {});
|
|
492
|
+
const raw = a.skill_name ?? a.skill ?? a.name;
|
|
493
|
+
if (typeof raw !== 'string' || !raw)
|
|
494
|
+
return null;
|
|
495
|
+
return normalizeSkillApprovalId(raw);
|
|
496
|
+
};
|
|
497
|
+
const skillId = deriveSkillId();
|
|
498
|
+
if (skillId) {
|
|
499
|
+
setLocalSkillApproval(prev => {
|
|
500
|
+
const next = new Map(prev);
|
|
501
|
+
next.set(skillId, approved);
|
|
502
|
+
return next;
|
|
503
|
+
});
|
|
504
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
505
|
+
type: approved ? 'skill_approve' : 'skill_unapprove',
|
|
506
|
+
skillId,
|
|
507
|
+
}, activeAgentId);
|
|
508
|
+
if (!ok) {
|
|
509
|
+
console.warn('[ChatBase] skill_approve persistence dropped: websocket not ready');
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// MCP tool approvals: tool_name is ``<serverId>__<toolName>``.
|
|
514
|
+
const sep = toolName.indexOf('__');
|
|
515
|
+
if (sep !== -1) {
|
|
516
|
+
const serverId = toolName.slice(0, sep);
|
|
517
|
+
const toolOnly = toolName.slice(sep + 2);
|
|
518
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
519
|
+
type: 'mcp_server_tool_approve',
|
|
520
|
+
serverId,
|
|
521
|
+
toolName: toolOnly,
|
|
522
|
+
approved,
|
|
523
|
+
}, activeAgentId);
|
|
524
|
+
if (!ok) {
|
|
525
|
+
console.warn('[ChatBase] mcp_server_tool_approve persistence dropped: websocket not ready');
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}, [activeAgentId]);
|
|
529
|
+
// Built-in approve/reject: send decisions to the runtime WS only.
|
|
530
|
+
// Approval state updates are sourced from the ai-agents WS listener.
|
|
531
|
+
const onApproveApproval = useCallback(async (approvalId, note, toolCallId) => {
|
|
532
|
+
const approval = agentRuntimeStore
|
|
533
|
+
.getState()
|
|
534
|
+
.approvals.find(a => a.id === approvalId);
|
|
535
|
+
const resolvedToolCallId = approval?.tool_call_id ?? toolCallId;
|
|
536
|
+
rememberResolvedToolCall(resolvedToolCallId);
|
|
537
|
+
// Persist approval decision to the ai-agents backend WS (single source
|
|
538
|
+
// of truth). This drives SaaS Tool Approvals state and broadcast events.
|
|
539
|
+
const persistedViaBackend = sendAiAgentsApprovalDecision(approvalId, true, note);
|
|
540
|
+
if (!persistedViaBackend) {
|
|
541
|
+
console.warn('[ChatBase] ai-agents tool_approval_decision not sent: websocket not ready');
|
|
542
|
+
}
|
|
543
|
+
// Keep the runtime decision path so the in-flight tool call can resume.
|
|
544
|
+
const runtimeOk = agentRuntimeStore
|
|
545
|
+
.getState()
|
|
546
|
+
.sendDecision(approvalId, true, note, resolvedToolCallId, activeAgentId);
|
|
547
|
+
if (runtimeOk) {
|
|
548
|
+
// Persist non-approval decisions locally; tool approvals are reconciled
|
|
549
|
+
// from ai-agents WS events/history to keep a single source of truth.
|
|
550
|
+
persistApprovalDecision(approvalId, true);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
console.warn('[ChatBase] tool_approval_decision dropped: websocket not ready');
|
|
554
|
+
}
|
|
555
|
+
if (persistedViaBackend || runtimeOk) {
|
|
556
|
+
// Optimistically clear local pending UI so completed tool calls do not
|
|
557
|
+
// stay pinned when websocket reconciliation is delayed.
|
|
558
|
+
agentRuntimeStore.getState().removeApproval(approvalId);
|
|
559
|
+
}
|
|
560
|
+
requestAiAgentsApprovalHistory();
|
|
561
|
+
queueApprovalDecisionRetry(approvalId, true, note);
|
|
562
|
+
await onApproveApprovalProp?.(approvalId, note);
|
|
563
|
+
}, [
|
|
564
|
+
activeAgentId,
|
|
565
|
+
rememberResolvedToolCall,
|
|
566
|
+
onApproveApprovalProp,
|
|
567
|
+
persistApprovalDecision,
|
|
568
|
+
queueApprovalDecisionRetry,
|
|
569
|
+
requestAiAgentsApprovalHistory,
|
|
570
|
+
sendAiAgentsApprovalDecision,
|
|
571
|
+
]);
|
|
572
|
+
const onRejectApproval = useCallback(async (approvalId, note, toolCallId) => {
|
|
573
|
+
const approval = agentRuntimeStore
|
|
574
|
+
.getState()
|
|
575
|
+
.approvals.find(a => a.id === approvalId);
|
|
576
|
+
const resolvedToolCallId = approval?.tool_call_id ?? toolCallId;
|
|
577
|
+
rememberResolvedToolCall(resolvedToolCallId);
|
|
578
|
+
const persistedViaBackend = sendAiAgentsApprovalDecision(approvalId, false, note);
|
|
579
|
+
if (!persistedViaBackend) {
|
|
580
|
+
console.warn('[ChatBase] ai-agents tool_approval_decision not sent: websocket not ready');
|
|
581
|
+
}
|
|
582
|
+
const runtimeOk = agentRuntimeStore
|
|
583
|
+
.getState()
|
|
584
|
+
.sendDecision(approvalId, false, note, resolvedToolCallId, activeAgentId);
|
|
585
|
+
if (runtimeOk) {
|
|
586
|
+
// Tool approval list is reconciled from ai-agents WS updates/history.
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
console.warn('[ChatBase] tool_approval_decision dropped: websocket not ready');
|
|
590
|
+
}
|
|
591
|
+
if (persistedViaBackend || runtimeOk) {
|
|
592
|
+
// Optimistically clear local pending UI so completed tool calls do not
|
|
593
|
+
// stay pinned when websocket reconciliation is delayed.
|
|
594
|
+
agentRuntimeStore.getState().removeApproval(approvalId);
|
|
595
|
+
}
|
|
596
|
+
requestAiAgentsApprovalHistory();
|
|
597
|
+
queueApprovalDecisionRetry(approvalId, false, note);
|
|
598
|
+
await onRejectApprovalProp?.(approvalId, note);
|
|
599
|
+
}, [
|
|
600
|
+
activeAgentId,
|
|
601
|
+
rememberResolvedToolCall,
|
|
602
|
+
onRejectApprovalProp,
|
|
603
|
+
queueApprovalDecisionRetry,
|
|
604
|
+
requestAiAgentsApprovalHistory,
|
|
605
|
+
sendAiAgentsApprovalDecision,
|
|
606
|
+
]);
|
|
607
|
+
// Optional ai-agents bridge for server-mode visibility.
|
|
608
|
+
// This keeps approval synchronization in ChatBase so examples do not need
|
|
609
|
+
// their own approval websocket plumbing.
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
if (!showToolApprovalBanner || pendingApprovalsProp) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (!aiAgentsAuthToken) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const wsUrl = toWsUrl(aiAgentsBaseUrl, `${AI_AGENTS_API_PREFIX}/ws`, aiAgentsAuthToken);
|
|
618
|
+
if (!wsUrl) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
let closedByCleanup = false;
|
|
622
|
+
const ws = new WebSocket(wsUrl);
|
|
623
|
+
aiAgentsApprovalWsRef.current = ws;
|
|
624
|
+
ws.onopen = () => {
|
|
625
|
+
logApprovalTrace('ws_open_request_history', {
|
|
626
|
+
activeAgentId,
|
|
627
|
+
});
|
|
628
|
+
ws.send(JSON.stringify({ type: 'tool-approvals-history' }));
|
|
629
|
+
};
|
|
630
|
+
ws.onmessage = (event) => {
|
|
631
|
+
try {
|
|
632
|
+
const raw = JSON.parse(String(event.data));
|
|
633
|
+
const records = [];
|
|
634
|
+
const msgType = typeof raw.type === 'string' ? raw.type : undefined;
|
|
635
|
+
const msgEvent = typeof raw.event === 'string' ? raw.event : undefined;
|
|
636
|
+
if (msgType === 'tool-approvals-history') {
|
|
637
|
+
const data = raw.data && typeof raw.data === 'object'
|
|
638
|
+
? raw.data
|
|
639
|
+
: {};
|
|
640
|
+
const approvals = data.approvals;
|
|
641
|
+
if (Array.isArray(approvals)) {
|
|
642
|
+
for (const item of approvals) {
|
|
643
|
+
if (item && typeof item === 'object') {
|
|
644
|
+
records.push(item);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else if (msgEvent?.startsWith('tool_approval_')) {
|
|
650
|
+
const data = raw.data && typeof raw.data === 'object'
|
|
651
|
+
? raw.data
|
|
652
|
+
: raw.payload && typeof raw.payload === 'object'
|
|
653
|
+
? raw.payload
|
|
654
|
+
: null;
|
|
655
|
+
if (data) {
|
|
656
|
+
const eventStatus = statusFromApprovalEvent(msgEvent);
|
|
657
|
+
logApprovalTrace('recv_tool_approval_event', {
|
|
658
|
+
event: msgEvent,
|
|
659
|
+
approvalId: typeof data.id === 'string'
|
|
660
|
+
? data.id
|
|
661
|
+
: typeof data.approval_id === 'string'
|
|
662
|
+
? data.approval_id
|
|
663
|
+
: undefined,
|
|
664
|
+
status: typeof data.status === 'string' ? data.status : eventStatus,
|
|
665
|
+
});
|
|
666
|
+
records.push(eventStatus && typeof data.status !== 'string'
|
|
667
|
+
? { ...data, status: eventStatus }
|
|
668
|
+
: data);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (records.length === 0) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const state = agentRuntimeStore.getState();
|
|
675
|
+
for (const record of records) {
|
|
676
|
+
const approval = normalizeApprovalPayload(record);
|
|
677
|
+
if (!approval) {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const scopedApproval = approval.agent_id || !activeAgentId
|
|
681
|
+
? approval
|
|
682
|
+
: { ...approval, agent_id: activeAgentId };
|
|
683
|
+
if (!isApprovalForAgent(scopedApproval, activeAgentId)) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (scopedApproval.status === 'pending' &&
|
|
687
|
+
isSuppressedPending(scopedApproval.tool_call_id)) {
|
|
688
|
+
logApprovalTrace('drop_transient_pending', {
|
|
689
|
+
approvalId: scopedApproval.id,
|
|
690
|
+
toolCallId: scopedApproval.tool_call_id,
|
|
691
|
+
status: scopedApproval.status,
|
|
692
|
+
});
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
logApprovalTrace('apply_tool_approval_update', {
|
|
696
|
+
approvalId: scopedApproval.id,
|
|
697
|
+
status: scopedApproval.status,
|
|
698
|
+
agentId: scopedApproval.agent_id,
|
|
699
|
+
activeAgentId,
|
|
700
|
+
});
|
|
701
|
+
if (scopedApproval.status === 'pending') {
|
|
702
|
+
state.upsertApproval(scopedApproval);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
state.removeApproval(scopedApproval.id);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// Ignore malformed payloads.
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
ws.onclose = () => {
|
|
714
|
+
if (!closedByCleanup) {
|
|
715
|
+
aiAgentsApprovalWsRef.current = null;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
ws.onerror = () => {
|
|
719
|
+
aiAgentsApprovalWsRef.current = null;
|
|
720
|
+
};
|
|
721
|
+
return () => {
|
|
722
|
+
closedByCleanup = true;
|
|
723
|
+
aiAgentsApprovalWsRef.current = null;
|
|
724
|
+
ws.close();
|
|
725
|
+
};
|
|
726
|
+
}, [
|
|
727
|
+
showToolApprovalBanner,
|
|
728
|
+
pendingApprovalsProp,
|
|
729
|
+
aiAgentsBaseUrl,
|
|
730
|
+
aiAgentsAuthToken,
|
|
731
|
+
activeAgentId,
|
|
732
|
+
isSuppressedPending,
|
|
733
|
+
]);
|
|
117
734
|
// The outer ChatBase wrapper always resolves a string Protocol to a full
|
|
118
735
|
// ProtocolConfig (or undefined). Narrow the type for internal use.
|
|
119
736
|
const protocol = typeof protocolRaw === 'object' ? protocolRaw : undefined;
|
|
120
737
|
// Stabilize the protocol reference so that the adapter-init effect only
|
|
121
738
|
// re-runs when the protocol *contents* actually change.
|
|
122
739
|
const protocolKey = protocol ? JSON.stringify(protocol) : '';
|
|
740
|
+
const monitoringServiceName = 'agent-runtimes';
|
|
123
741
|
// Store (optional for message persistence)
|
|
124
742
|
const clearStoreMessages = useChatStore(state => state.clearMessages);
|
|
125
743
|
// Check if protocol is A2A (doesn't support per-request model override)
|
|
@@ -127,11 +745,28 @@ pendingPrompt, }) {
|
|
|
127
745
|
// ---- Component state ----
|
|
128
746
|
const [displayItems, setDisplayItems] = useState([]);
|
|
129
747
|
const [isLoading, setIsLoading] = useState(false);
|
|
748
|
+
const [liveKernelStatus, setLiveKernelStatus] = useState();
|
|
130
749
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
131
750
|
const [error, setError] = useState(null);
|
|
132
751
|
const [input, setInput] = useState('');
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
if (!kernel) {
|
|
754
|
+
setLiveKernelStatus(undefined);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
setLiveKernelStatus(kernel.status);
|
|
758
|
+
const handleStatusChange = (_, nextStatus) => {
|
|
759
|
+
setLiveKernelStatus(nextStatus);
|
|
760
|
+
};
|
|
761
|
+
kernel.statusChanged.connect(handleStatusChange);
|
|
762
|
+
return () => {
|
|
763
|
+
kernel.statusChanged.disconnect(handleStatusChange);
|
|
764
|
+
};
|
|
765
|
+
}, [kernel]);
|
|
133
766
|
// History-loaded flag — true immediately when there is nothing to fetch
|
|
134
|
-
const [historyLoaded, setHistoryLoaded] = useState(!
|
|
767
|
+
const [historyLoaded, setHistoryLoaded] = useState(!historyScopeId);
|
|
768
|
+
const [historyRefreshTick, setHistoryRefreshTick] = useState(0);
|
|
769
|
+
const historyRetryAttemptsRef = useRef(new Map());
|
|
135
770
|
// Adapter-ready flag — flipped to true once the protocol adapter is initialised
|
|
136
771
|
const [adapterReady, setAdapterReady] = useState(false);
|
|
137
772
|
// Guard so the pending prompt is sent at most once
|
|
@@ -147,27 +782,99 @@ pendingPrompt, }) {
|
|
|
147
782
|
// enabledTools tracks which MCP server tools are enabled
|
|
148
783
|
// Format: Map<serverId, Set<toolName>>
|
|
149
784
|
const [enabledMcpTools, setEnabledMcpTools] = useState(new Map());
|
|
785
|
+
// approvedMcpTools tracks which MCP server tools are approved per server.
|
|
786
|
+
// Default: no tools approved until explicitly toggled.
|
|
787
|
+
const [approvedMcpTools, setApprovedMcpTools] = useState(new Map());
|
|
150
788
|
// Note: legacy _enabledTools for backend-defined tools from config query
|
|
151
789
|
const [_enabledTools, setEnabledTools] = useState([]);
|
|
152
|
-
|
|
153
|
-
const [enabledSkills, setEnabledSkills] = useState(new Set());
|
|
790
|
+
const wsState = useAgentRuntimeWsState();
|
|
154
791
|
// ---- Data queries ----
|
|
155
|
-
const configQuery = useConfig(Boolean(protocol?.enableConfigQuery), protocol?.configEndpoint, protocol?.authToken);
|
|
792
|
+
const configQuery = useConfig(Boolean(protocol?.enableConfigQuery), protocol?.configEndpoint, protocol?.authToken, protocol?.agentId);
|
|
156
793
|
const skillsQuery = useSkills(Boolean(protocol?.enableConfigQuery) && showSkillsMenu, protocol?.configEndpoint, protocol?.authToken);
|
|
794
|
+
const { enableSkill: wsEnableSkill, disableSkill: wsDisableSkill, approveSkill: wsApproveSkill, unapproveSkill: wsUnapproveSkill, } = useSkillActions(activeAgentId);
|
|
795
|
+
// Optimistic skill-approval overrides so inline approval updates the
|
|
796
|
+
// Skills selector immediately before the next WS snapshot lands.
|
|
797
|
+
const [localSkillApproval, setLocalSkillApproval] = useState(new Map());
|
|
798
|
+
useEffect(() => {
|
|
799
|
+
setLocalSkillApproval(new Map());
|
|
800
|
+
}, [activeAgentId]);
|
|
801
|
+
// Derive enabledSkills from the WS-pushed skill statuses.
|
|
802
|
+
const enabledSkills = useMemo(() => {
|
|
803
|
+
const set = new Set();
|
|
804
|
+
for (const s of skillsQuery.data?.skills ?? []) {
|
|
805
|
+
if (s.status === 'enabled' || s.status === 'loaded') {
|
|
806
|
+
set.add(s.id);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return set;
|
|
810
|
+
}, [skillsQuery.data]);
|
|
811
|
+
// Backward-compatibility bootstrap: if a running agent reports skills as
|
|
812
|
+
// available-only, enable them once. This is gated per agent so explicit
|
|
813
|
+
// user disable actions are not overridden later.
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
if (!activeAgentId) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (defaultSkillsBootstrapRef.current.has(activeAgentId)) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const skills = skillsQuery.data?.skills ?? [];
|
|
822
|
+
if (skills.length === 0) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const hasAnyEnabled = skills.some(s => s.status === 'enabled' || s.status === 'loaded');
|
|
826
|
+
if (hasAnyEnabled) {
|
|
827
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const availableSkillIds = skills
|
|
831
|
+
.filter(s => s.status === 'available')
|
|
832
|
+
.map(s => s.id)
|
|
833
|
+
.filter(Boolean);
|
|
834
|
+
if (availableSkillIds.length === 0) {
|
|
835
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const allSent = availableSkillIds.every(skillId => wsEnableSkill(skillId));
|
|
839
|
+
if (allSent) {
|
|
840
|
+
defaultSkillsBootstrapRef.current.add(activeAgentId);
|
|
841
|
+
}
|
|
842
|
+
}, [activeAgentId, skillsQuery.data, wsEnableSkill]);
|
|
843
|
+
// Derive approvedSkills from the WS-pushed skill statuses (default: not approved).
|
|
844
|
+
const approvedSkills = useMemo(() => {
|
|
845
|
+
const set = new Set();
|
|
846
|
+
for (const s of skillsQuery.data?.skills ?? []) {
|
|
847
|
+
if (s.approved === true) {
|
|
848
|
+
set.add(s.id);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
localSkillApproval.forEach((approved, skillId) => {
|
|
852
|
+
if (approved) {
|
|
853
|
+
set.add(skillId);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
set.delete(skillId);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
return set;
|
|
860
|
+
}, [localSkillApproval, skillsQuery.data]);
|
|
157
861
|
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;
|
|
862
|
+
const agentUsage = externalContextSnapshot ?? contextSnapshotQuery.data;
|
|
863
|
+
const sandboxStatusQuery = useSandbox(Boolean(protocol?.enableConfigQuery) && showHeader, protocol?.configEndpoint, protocol?.authToken, protocol?.agentId);
|
|
161
864
|
// ---- Refs ----
|
|
162
865
|
const adapterRef = useRef(null);
|
|
163
866
|
const unsubscribeRef = useRef(null);
|
|
164
867
|
const toolCallsRef = useRef(new Map());
|
|
165
868
|
const pendingToolExecutionsRef = useRef(0);
|
|
166
869
|
const currentAssistantMessageRef = useRef(null);
|
|
870
|
+
const respondedApprovalIdsRef = useRef(new Set());
|
|
871
|
+
const defaultSkillsBootstrapRef = useRef(new Set());
|
|
872
|
+
const defaultMcpToolsBootstrapRef = useRef(new Set());
|
|
167
873
|
const suppressAssistantTextForToolOnlyRef = useRef(false);
|
|
168
874
|
const hideMessagesAfterToolUIRef = useRef(hideMessagesAfterToolUI);
|
|
169
875
|
hideMessagesAfterToolUIRef.current = hideMessagesAfterToolUI;
|
|
170
876
|
const threadIdRef = useRef(generateMessageId());
|
|
877
|
+
const messagesContainerRef = useRef(null);
|
|
171
878
|
const messagesEndRef = useRef(null);
|
|
172
879
|
const inputRef = useRef(null);
|
|
173
880
|
const abortControllerRef = useRef(null);
|
|
@@ -180,6 +887,86 @@ pendingPrompt, }) {
|
|
|
180
887
|
// re-created when frontendTools changes) always accesses the latest value.
|
|
181
888
|
const frontendToolsRef = useRef(frontendTools);
|
|
182
889
|
frontendToolsRef.current = frontendTools;
|
|
890
|
+
// Stable refs for tool invocation hooks (pre/post)
|
|
891
|
+
const onToolCallStartRef = useRef(onToolCallStart);
|
|
892
|
+
onToolCallStartRef.current = onToolCallStart;
|
|
893
|
+
const onToolCallCompleteRef = useRef(onToolCallComplete);
|
|
894
|
+
onToolCallCompleteRef.current = onToolCallComplete;
|
|
895
|
+
const handleRespondRef = useRef(null);
|
|
896
|
+
const applyServerApprovalDecision = useCallback((approval, approved, note) => {
|
|
897
|
+
if (!approval?.id) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
if (respondedApprovalIdsRef.current.has(approval.id)) {
|
|
901
|
+
return false;
|
|
902
|
+
}
|
|
903
|
+
let targetToolCallId = approval.tool_call_id;
|
|
904
|
+
if (!targetToolCallId) {
|
|
905
|
+
for (const [tcId, tc] of toolCallsRef.current.entries()) {
|
|
906
|
+
if (tc.status !== 'inProgress' && tc.status !== 'executing') {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const tcSig = approvalSignature(tc.toolName, tc.args ?? {});
|
|
910
|
+
const approvalSig = approvalSignature(approval.tool_name, approval.tool_args ?? {});
|
|
911
|
+
if (tcSig === approvalSig) {
|
|
912
|
+
targetToolCallId = tcId;
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!targetToolCallId) {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
const target = toolCallsRef.current.get(targetToolCallId);
|
|
921
|
+
if (!target) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
if (target.status !== 'inProgress' && target.status !== 'executing') {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
respondedApprovalIdsRef.current.add(approval.id);
|
|
928
|
+
void handleRespondRef.current?.(targetToolCallId, {
|
|
929
|
+
type: 'tool-approval-decision',
|
|
930
|
+
approved,
|
|
931
|
+
approvalId: approval.id,
|
|
932
|
+
toolName: approval.tool_name || target.toolName,
|
|
933
|
+
_fromServerEcho: true,
|
|
934
|
+
_alreadyDispatched: true,
|
|
935
|
+
...(note ? { message: note } : {}),
|
|
936
|
+
});
|
|
937
|
+
return true;
|
|
938
|
+
}, [activeAgentId]);
|
|
939
|
+
// ---- Agent-runtime WebSocket (monitoring stream) ----
|
|
940
|
+
// Derive the bare base URL from configEndpoint or protocol.endpoint.
|
|
941
|
+
const wsBaseUrl = protocol?.configEndpoint
|
|
942
|
+
? protocol.configEndpoint.replace(/\/api\/v1\/(config|configure)\/?$/, '')
|
|
943
|
+
: (protocol?.endpoint?.replace(/\/api\/v1\/.*$/, '') ?? '');
|
|
944
|
+
useAgentRuntimeWebSocket({
|
|
945
|
+
enabled: !!protocol && !!wsBaseUrl,
|
|
946
|
+
baseUrl: wsBaseUrl,
|
|
947
|
+
authToken: protocol?.authToken,
|
|
948
|
+
agentId: protocol?.agentId,
|
|
949
|
+
onMessage: msg => {
|
|
950
|
+
if (msg.type !== 'tool_approval_created' &&
|
|
951
|
+
msg.type !== 'tool_approval_approved' &&
|
|
952
|
+
msg.type !== 'tool_approval_rejected') {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const payload = msg.payload;
|
|
956
|
+
if (!payload) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
// Keep the top approval banner populated in local/no-token flows by
|
|
960
|
+
// ingesting pending approval events from the runtime stream.
|
|
961
|
+
if (msg.type === 'tool_approval_created') {
|
|
962
|
+
if (isApprovalForAgent(payload, activeAgentId)) {
|
|
963
|
+
agentRuntimeStore.getState().upsertApproval(payload);
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
applyServerApprovalDecision(payload, msg.type === 'tool_approval_approved', payload.note ?? undefined);
|
|
968
|
+
},
|
|
969
|
+
});
|
|
183
970
|
// ---- Helpers ----
|
|
184
971
|
const isServerSelected = useCallback((server) => {
|
|
185
972
|
if (!mcpServers)
|
|
@@ -259,7 +1046,13 @@ pendingPrompt, }) {
|
|
|
259
1046
|
if (server.isAvailable && server.enabled) {
|
|
260
1047
|
const shouldEnableServer = isServerSelected(server);
|
|
261
1048
|
if (shouldEnableServer) {
|
|
262
|
-
|
|
1049
|
+
// Default to "all tools enabled" so the dropdown reflects an
|
|
1050
|
+
// immediately-usable state. The WS sync effect will reconcile
|
|
1051
|
+
// with server-side state once it arrives, and user toggles
|
|
1052
|
+
// win after that.
|
|
1053
|
+
const enabledToolNames = new Set(server.tools
|
|
1054
|
+
.map(t => t.name)
|
|
1055
|
+
.filter((name) => Boolean(name)));
|
|
263
1056
|
newEnabledMcpTools.set(server.id, enabledToolNames);
|
|
264
1057
|
}
|
|
265
1058
|
}
|
|
@@ -283,24 +1076,147 @@ pendingPrompt, }) {
|
|
|
283
1076
|
const newMap = new Map();
|
|
284
1077
|
for (const server of configQuery.data?.mcpServers ?? []) {
|
|
285
1078
|
if (isServerSelected(server) && prev.has(server.id)) {
|
|
286
|
-
|
|
1079
|
+
const existing = prev.get(server.id);
|
|
1080
|
+
if (existing)
|
|
1081
|
+
newMap.set(server.id, existing);
|
|
287
1082
|
}
|
|
288
1083
|
else if (isServerSelected(server) &&
|
|
289
1084
|
server.isAvailable &&
|
|
290
1085
|
server.enabled) {
|
|
291
|
-
const enabledToolNames = new Set(server.tools
|
|
1086
|
+
const enabledToolNames = new Set(server.tools
|
|
1087
|
+
.map(t => t.name)
|
|
1088
|
+
.filter((name) => Boolean(name)));
|
|
292
1089
|
newMap.set(server.id, enabledToolNames);
|
|
293
1090
|
}
|
|
294
1091
|
}
|
|
295
1092
|
return newMap;
|
|
296
1093
|
});
|
|
297
1094
|
}, [mcpServers, configQuery.data?.mcpServers, isServerSelected]);
|
|
298
|
-
//
|
|
1095
|
+
// Keep MCP tool selection synchronized with backend WS snapshots.
|
|
1096
|
+
// On first load per agent, if server state reports no enabled tools,
|
|
1097
|
+
// bootstrap to "all enabled" from config so codemode starts usable by
|
|
1098
|
+
// default. Later user toggles still win because bootstrap runs once.
|
|
1099
|
+
const mcpServersRef = useRef(mcpServers);
|
|
1100
|
+
mcpServersRef.current = mcpServers;
|
|
299
1101
|
useEffect(() => {
|
|
300
|
-
|
|
301
|
-
|
|
1102
|
+
const wsEnabledMcpTools = parseEnabledMcpToolsByServer(effectiveMcpStatusData);
|
|
1103
|
+
if (!wsEnabledMcpTools) {
|
|
1104
|
+
return;
|
|
302
1105
|
}
|
|
303
|
-
|
|
1106
|
+
const bootstrapAgentKey = activeAgentId || '__global__';
|
|
1107
|
+
const shouldBootstrap = !defaultMcpToolsBootstrapRef.current.has(bootstrapAgentKey) &&
|
|
1108
|
+
wsState === 'connected';
|
|
1109
|
+
setEnabledMcpTools(prev => {
|
|
1110
|
+
const next = new Map(prev);
|
|
1111
|
+
// Apply WS state per server, but only when it carries an authoritative,
|
|
1112
|
+
// non-empty list. An empty list from WS is treated as "no information"
|
|
1113
|
+
// (likely a transient snapshot before backend defaults are projected)
|
|
1114
|
+
// so we keep whatever the user / bootstrap already set, preventing
|
|
1115
|
+
// the dropdown from flickering between enabled and disabled states.
|
|
1116
|
+
wsEnabledMcpTools.forEach((toolNames, serverId) => {
|
|
1117
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
1118
|
+
mcpServersRef.current.some(server => server.id === serverId);
|
|
1119
|
+
if (!selectedInProps) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (toolNames.size === 0) {
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
next.set(serverId, new Set(toolNames));
|
|
1126
|
+
});
|
|
1127
|
+
if (shouldBootstrap) {
|
|
1128
|
+
const bootstrapMessages = [];
|
|
1129
|
+
for (const server of configQuery.data?.mcpServers ?? []) {
|
|
1130
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
1131
|
+
mcpServersRef.current.some(s => s.id === server.id);
|
|
1132
|
+
if (!selectedInProps || !server.isAvailable || !server.enabled) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const allToolNames = server.tools
|
|
1136
|
+
.map(t => t.name)
|
|
1137
|
+
.filter((name) => Boolean(name));
|
|
1138
|
+
if (allToolNames.length === 0) {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
const current = next.get(server.id);
|
|
1142
|
+
if (!current || current.size === 0) {
|
|
1143
|
+
next.set(server.id, new Set(allToolNames));
|
|
1144
|
+
bootstrapMessages.push({
|
|
1145
|
+
serverId: server.id,
|
|
1146
|
+
enabledToolNames: allToolNames,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
let allMessagesSent = true;
|
|
1151
|
+
for (const msg of bootstrapMessages) {
|
|
1152
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1153
|
+
type: 'mcp_server_tools_set',
|
|
1154
|
+
serverId: msg.serverId,
|
|
1155
|
+
enabledToolNames: msg.enabledToolNames,
|
|
1156
|
+
}, activeAgentId);
|
|
1157
|
+
if (!ok) {
|
|
1158
|
+
allMessagesSent = false;
|
|
1159
|
+
console.warn('[ChatBase] initial mcp_server_tools_set dropped: websocket not ready');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (allMessagesSent) {
|
|
1163
|
+
defaultMcpToolsBootstrapRef.current.add(bootstrapAgentKey);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return next;
|
|
1167
|
+
});
|
|
1168
|
+
}, [
|
|
1169
|
+
effectiveMcpStatusData,
|
|
1170
|
+
activeAgentId,
|
|
1171
|
+
configQuery.data?.mcpServers,
|
|
1172
|
+
wsState,
|
|
1173
|
+
]);
|
|
1174
|
+
// Keep MCP tool *approval* synchronized with backend WS snapshots.
|
|
1175
|
+
useEffect(() => {
|
|
1176
|
+
const wsApprovedMcpTools = parseApprovedMcpToolsByServer(effectiveMcpStatusData);
|
|
1177
|
+
if (!wsApprovedMcpTools) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
setApprovedMcpTools(() => {
|
|
1181
|
+
const next = new Map();
|
|
1182
|
+
wsApprovedMcpTools.forEach((toolNames, serverId) => {
|
|
1183
|
+
const selectedInProps = !mcpServersRef.current ||
|
|
1184
|
+
mcpServersRef.current.some(server => server.id === serverId);
|
|
1185
|
+
if (selectedInProps) {
|
|
1186
|
+
next.set(serverId, new Set(toolNames));
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
return next;
|
|
1190
|
+
});
|
|
1191
|
+
}, [effectiveMcpStatusData]);
|
|
1192
|
+
// Refetch configQuery when WS reports MCP servers as started but the
|
|
1193
|
+
// cached config response has missing servers or empty tools.
|
|
1194
|
+
const lastConfigMcpKeyRef = useRef('');
|
|
1195
|
+
useEffect(() => {
|
|
1196
|
+
const wsServers = effectiveMcpStatusData?.servers;
|
|
1197
|
+
if (!wsServers || wsServers.length === 0)
|
|
1198
|
+
return;
|
|
1199
|
+
const startedIds = wsServers
|
|
1200
|
+
.filter(s => s.status === 'started')
|
|
1201
|
+
.map(s => s.id)
|
|
1202
|
+
.sort();
|
|
1203
|
+
if (startedIds.length === 0)
|
|
1204
|
+
return;
|
|
1205
|
+
const configServers = configQuery.data?.mcpServers || [];
|
|
1206
|
+
const needsRefetch = startedIds.some(id => {
|
|
1207
|
+
const cs = configServers.find(s => s.id === id);
|
|
1208
|
+
return !cs || cs.tools.length === 0;
|
|
1209
|
+
});
|
|
1210
|
+
// Only refetch once per unique set of started server IDs
|
|
1211
|
+
const key = startedIds.join(',');
|
|
1212
|
+
if (needsRefetch &&
|
|
1213
|
+
key !== lastConfigMcpKeyRef.current &&
|
|
1214
|
+
configQuery.refetch) {
|
|
1215
|
+
lastConfigMcpKeyRef.current = key;
|
|
1216
|
+
configQuery.refetch();
|
|
1217
|
+
}
|
|
1218
|
+
}, [effectiveMcpStatusData, configQuery]);
|
|
1219
|
+
// initialSkills are now handled server-side during agent creation.
|
|
304
1220
|
// ---- Toggle helpers ----
|
|
305
1221
|
const toggleMcpTool = useCallback((serverId, toolName) => {
|
|
306
1222
|
setEnabledMcpTools(prev => {
|
|
@@ -313,36 +1229,99 @@ pendingPrompt, }) {
|
|
|
313
1229
|
serverTools.add(toolName);
|
|
314
1230
|
}
|
|
315
1231
|
newMap.set(serverId, serverTools);
|
|
1232
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1233
|
+
type: 'mcp_server_tools_set',
|
|
1234
|
+
serverId,
|
|
1235
|
+
enabledToolNames: Array.from(serverTools),
|
|
1236
|
+
}, activeAgentId);
|
|
1237
|
+
if (!ok) {
|
|
1238
|
+
console.warn('[ChatBase] mcp_server_tools_set dropped: websocket not ready');
|
|
1239
|
+
}
|
|
316
1240
|
return newMap;
|
|
317
1241
|
});
|
|
318
|
-
}, []);
|
|
1242
|
+
}, [activeAgentId]);
|
|
319
1243
|
const toggleAllMcpServerTools = useCallback((serverId, allToolNames, enable) => {
|
|
320
1244
|
setEnabledMcpTools(prev => {
|
|
321
1245
|
const newMap = new Map(prev);
|
|
1246
|
+
const nextTools = enable ? new Set(allToolNames) : new Set();
|
|
322
1247
|
if (enable) {
|
|
323
|
-
newMap.set(serverId,
|
|
1248
|
+
newMap.set(serverId, nextTools);
|
|
324
1249
|
}
|
|
325
1250
|
else {
|
|
326
|
-
newMap.set(serverId,
|
|
1251
|
+
newMap.set(serverId, nextTools);
|
|
1252
|
+
}
|
|
1253
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1254
|
+
type: 'mcp_server_tools_set',
|
|
1255
|
+
serverId,
|
|
1256
|
+
enabledToolNames: Array.from(nextTools),
|
|
1257
|
+
}, activeAgentId);
|
|
1258
|
+
if (!ok) {
|
|
1259
|
+
console.warn('[ChatBase] mcp_server_tools_set dropped: websocket not ready');
|
|
327
1260
|
}
|
|
328
1261
|
return newMap;
|
|
329
1262
|
});
|
|
330
|
-
}, []);
|
|
1263
|
+
}, [activeAgentId]);
|
|
331
1264
|
const toggleSkill = useCallback((skillId) => {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
1265
|
+
if (enabledSkills.has(skillId)) {
|
|
1266
|
+
wsDisableSkill(skillId);
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
wsEnableSkill(skillId);
|
|
1270
|
+
}
|
|
1271
|
+
}, [enabledSkills, wsEnableSkill, wsDisableSkill]);
|
|
1272
|
+
const toggleAllSkills = useCallback((allSkillIds, enable) => {
|
|
1273
|
+
for (const id of allSkillIds) {
|
|
1274
|
+
if (enable) {
|
|
1275
|
+
wsEnableSkill(id);
|
|
336
1276
|
}
|
|
337
1277
|
else {
|
|
338
|
-
|
|
1278
|
+
wsDisableSkill(id);
|
|
339
1279
|
}
|
|
340
|
-
|
|
1280
|
+
}
|
|
1281
|
+
}, [wsEnableSkill, wsDisableSkill]);
|
|
1282
|
+
const toggleMcpToolApproval = useCallback((serverId, toolName) => {
|
|
1283
|
+
setApprovedMcpTools(prev => {
|
|
1284
|
+
const newMap = new Map(prev);
|
|
1285
|
+
// Default: if no entry for this server, no tool is approved.
|
|
1286
|
+
const serverTools = new Set(prev.get(serverId) ?? []);
|
|
1287
|
+
const currentlyApproved = serverTools.has(toolName);
|
|
1288
|
+
if (currentlyApproved) {
|
|
1289
|
+
serverTools.delete(toolName);
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
serverTools.add(toolName);
|
|
1293
|
+
}
|
|
1294
|
+
newMap.set(serverId, serverTools);
|
|
1295
|
+
const ok = agentRuntimeStore.getState().sendRawMessage({
|
|
1296
|
+
type: 'mcp_server_tool_approve',
|
|
1297
|
+
serverId,
|
|
1298
|
+
toolName,
|
|
1299
|
+
approved: !currentlyApproved,
|
|
1300
|
+
}, activeAgentId);
|
|
1301
|
+
if (!ok) {
|
|
1302
|
+
console.warn('[ChatBase] mcp_server_tool_approve dropped: websocket not ready');
|
|
1303
|
+
}
|
|
1304
|
+
return newMap;
|
|
341
1305
|
});
|
|
342
|
-
}, []);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
1306
|
+
}, [activeAgentId]);
|
|
1307
|
+
const toggleSkillApproval = useCallback((skillId) => {
|
|
1308
|
+
if (approvedSkills.has(skillId)) {
|
|
1309
|
+
setLocalSkillApproval(prev => {
|
|
1310
|
+
const next = new Map(prev);
|
|
1311
|
+
next.set(skillId, false);
|
|
1312
|
+
return next;
|
|
1313
|
+
});
|
|
1314
|
+
wsUnapproveSkill(skillId);
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
setLocalSkillApproval(prev => {
|
|
1318
|
+
const next = new Map(prev);
|
|
1319
|
+
next.set(skillId, true);
|
|
1320
|
+
return next;
|
|
1321
|
+
});
|
|
1322
|
+
wsApproveSkill(skillId);
|
|
1323
|
+
}
|
|
1324
|
+
}, [approvedSkills, wsApproveSkill, wsUnapproveSkill]);
|
|
346
1325
|
const getEnabledMcpToolNames = useCallback(() => {
|
|
347
1326
|
const toolNames = [];
|
|
348
1327
|
enabledMcpTools.forEach((tools, serverId) => {
|
|
@@ -356,123 +1335,165 @@ pendingPrompt, }) {
|
|
|
356
1335
|
return Array.from(enabledSkills);
|
|
357
1336
|
}, [enabledSkills]);
|
|
358
1337
|
// ---- Load messages from store on mount ----
|
|
1338
|
+
// Only hydrate from the shared ``useChatStore`` when there is no
|
|
1339
|
+
// ``runtimeId`` (pure store mode without server-backed history). When a
|
|
1340
|
+
// ``runtimeId`` is provided the "Conversation history loading" effect
|
|
1341
|
+
// below is the single source of truth — reading from the shared store
|
|
1342
|
+
// here would otherwise leak messages from a previously-mounted
|
|
1343
|
+
// ``ChatBase`` (e.g. after switching examples) before the store reset
|
|
1344
|
+
// or history fetch completes.
|
|
359
1345
|
useEffect(() => {
|
|
360
|
-
if (useStoreMode) {
|
|
1346
|
+
if (useStoreMode && !runtimeId) {
|
|
361
1347
|
const storeMessages = useChatStore.getState().messages;
|
|
362
1348
|
if (storeMessages.length > 0) {
|
|
363
1349
|
setDisplayItems(storeMessages);
|
|
364
1350
|
}
|
|
365
1351
|
}
|
|
366
|
-
}, [useStoreMode]);
|
|
1352
|
+
}, [useStoreMode, runtimeId]);
|
|
367
1353
|
// ---- Conversation history loading ----
|
|
368
|
-
const
|
|
1354
|
+
const prevHistoryScopeRef = useRef(undefined);
|
|
369
1355
|
useEffect(() => {
|
|
370
|
-
if (
|
|
371
|
-
|
|
1356
|
+
if (historyScopeId !== prevHistoryScopeRef.current) {
|
|
1357
|
+
if (historyScopeId) {
|
|
1358
|
+
historyRetryAttemptsRef.current.set(historyScopeId, 0);
|
|
1359
|
+
}
|
|
1360
|
+
prevHistoryScopeRef.current = historyScopeId;
|
|
372
1361
|
setDisplayItems([]);
|
|
373
1362
|
toolCallsRef.current.clear();
|
|
374
|
-
if (!
|
|
1363
|
+
if (!historyScopeId)
|
|
375
1364
|
return;
|
|
376
1365
|
}
|
|
377
|
-
|
|
1366
|
+
if (!historyScopeId)
|
|
378
1367
|
return;
|
|
379
|
-
}
|
|
380
1368
|
const store = useConversationStore.getState();
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
1369
|
+
const currentlyFetching = store.isFetching(historyScopeId);
|
|
1370
|
+
const storedMessages = store.getMessages(historyScopeId);
|
|
1371
|
+
// 1) Fast local hydration for view switches in the same browser session.
|
|
1372
|
+
if (storedMessages.length > 0) {
|
|
1373
|
+
setDisplayItems(storedMessages);
|
|
386
1374
|
setHistoryLoaded(true);
|
|
1375
|
+
}
|
|
1376
|
+
if (currentlyFetching) {
|
|
387
1377
|
return;
|
|
388
1378
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
setHistoryLoaded(true);
|
|
1379
|
+
// 2) On refresh/mount, prefer websocket refresh from the runtime.
|
|
1380
|
+
// If the socket is not connected yet, keep retryable fetch state and wait.
|
|
1381
|
+
if (wsState !== 'connected') {
|
|
1382
|
+
if (storedMessages.length === 0) {
|
|
1383
|
+
setHistoryLoaded(false);
|
|
1384
|
+
}
|
|
396
1385
|
return;
|
|
397
1386
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1387
|
+
store.setFetching(historyScopeId, true);
|
|
1388
|
+
const fullContextToMessages = () => extractChatMessagesFromFullContext(agentRuntimeStore.getState().fullContext);
|
|
1389
|
+
const applyMessages = (messages) => {
|
|
1390
|
+
store.setMessages(historyScopeId, messages);
|
|
1391
|
+
setDisplayItems(convertHistoryToDisplayItems(messages));
|
|
1392
|
+
historyRetryAttemptsRef.current.set(historyScopeId, 0);
|
|
1393
|
+
store.markFetched(historyScopeId);
|
|
1394
|
+
setHistoryLoaded(true);
|
|
1395
|
+
};
|
|
1396
|
+
const existingMessages = fullContextToMessages();
|
|
1397
|
+
if (existingMessages.length > 0) {
|
|
1398
|
+
applyMessages(existingMessages);
|
|
1399
|
+
return;
|
|
401
1400
|
}
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
throw new Error(`Failed to fetch history: ${response.status} ${response.statusText}`);
|
|
1401
|
+
const requestSnapshotRefresh = () => {
|
|
1402
|
+
const candidates = [
|
|
1403
|
+
activeAgentId,
|
|
1404
|
+
protocolConfig?.agentId,
|
|
1405
|
+
runtimeId,
|
|
1406
|
+
historyScopeId,
|
|
1407
|
+
'default',
|
|
1408
|
+
undefined,
|
|
1409
|
+
];
|
|
1410
|
+
const tried = new Set();
|
|
1411
|
+
for (const candidate of candidates) {
|
|
1412
|
+
const normalized = typeof candidate === 'string' ? candidate.trim() : undefined;
|
|
1413
|
+
const key = normalized || '__global__';
|
|
1414
|
+
if (tried.has(key)) {
|
|
1415
|
+
continue;
|
|
418
1416
|
}
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
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);
|
|
1417
|
+
tried.add(key);
|
|
1418
|
+
const ok = agentRuntimeStore.getState().requestRefresh(normalized);
|
|
1419
|
+
if (ok) {
|
|
1420
|
+
return true;
|
|
449
1421
|
}
|
|
450
|
-
store.markFetched(runtimeId);
|
|
451
|
-
setHistoryLoaded(true);
|
|
452
1422
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1423
|
+
return false;
|
|
1424
|
+
};
|
|
1425
|
+
// Ask the monitoring websocket for a fresh snapshot and wait briefly
|
|
1426
|
+
// for `fullContext.messages` to arrive.
|
|
1427
|
+
const refreshRequested = requestSnapshotRefresh();
|
|
1428
|
+
if (!refreshRequested) {
|
|
1429
|
+
// Socket not ready yet; allow a later retry (e.g. when wsState changes).
|
|
1430
|
+
store.setFetching(historyScopeId, false);
|
|
1431
|
+
if (storedMessages.length === 0) {
|
|
1432
|
+
setHistoryLoaded(false);
|
|
1433
|
+
}
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
let resolved = false;
|
|
1437
|
+
const retryRefreshTimeout = window.setTimeout(() => {
|
|
1438
|
+
if (!resolved) {
|
|
1439
|
+
requestSnapshotRefresh();
|
|
1440
|
+
}
|
|
1441
|
+
}, 500);
|
|
1442
|
+
const unsubscribe = agentRuntimeStore.subscribe(state => state.fullContext, nextFullContext => {
|
|
1443
|
+
if (resolved || !nextFullContext) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
const messages = extractChatMessagesFromFullContext(nextFullContext);
|
|
1447
|
+
resolved = true;
|
|
1448
|
+
unsubscribe();
|
|
1449
|
+
applyMessages(messages);
|
|
1450
|
+
});
|
|
1451
|
+
const timeout = window.setTimeout(() => {
|
|
1452
|
+
if (resolved) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
resolved = true;
|
|
1456
|
+
unsubscribe();
|
|
1457
|
+
// Do not mark as fetched on timeout; keep it retryable for late WS snapshots.
|
|
1458
|
+
store.setFetching(historyScopeId, false);
|
|
1459
|
+
setHistoryLoaded(storedMessages.length > 0);
|
|
1460
|
+
const attempts = historyRetryAttemptsRef.current.get(historyScopeId) ?? 0;
|
|
1461
|
+
const canRetry = wsState === 'connected' && attempts < 3;
|
|
1462
|
+
if (canRetry) {
|
|
1463
|
+
historyRetryAttemptsRef.current.set(historyScopeId, attempts + 1);
|
|
1464
|
+
requestSnapshotRefresh();
|
|
1465
|
+
setHistoryRefreshTick(tick => tick + 1);
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
// After retries are exhausted, treat the conversation as loaded-empty
|
|
1469
|
+
// so pending prompts are not blocked forever on fresh runtimes.
|
|
456
1470
|
setHistoryLoaded(true);
|
|
457
1471
|
}
|
|
1472
|
+
}, 2000);
|
|
1473
|
+
return () => {
|
|
1474
|
+
window.clearTimeout(retryRefreshTimeout);
|
|
1475
|
+
window.clearTimeout(timeout);
|
|
1476
|
+
unsubscribe();
|
|
458
1477
|
};
|
|
459
|
-
fetchHistory();
|
|
460
1478
|
}, [
|
|
461
|
-
|
|
1479
|
+
historyScopeId,
|
|
462
1480
|
historyEndpoint,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1481
|
+
protocol?.agentId,
|
|
1482
|
+
wsState,
|
|
1483
|
+
activeAgentId,
|
|
1484
|
+
historyRefreshTick,
|
|
466
1485
|
]);
|
|
467
1486
|
// Keep in-memory store in sync with displayItems
|
|
468
1487
|
useEffect(() => {
|
|
469
|
-
if (
|
|
1488
|
+
if (historyScopeId && displayItems.length > 0) {
|
|
470
1489
|
const messagesToSave = displayItems.filter((item) => !isToolCallMessage(item));
|
|
471
1490
|
if (messagesToSave.length > 0) {
|
|
472
|
-
useConversationStore
|
|
1491
|
+
useConversationStore
|
|
1492
|
+
.getState()
|
|
1493
|
+
.setMessages(historyScopeId, messagesToSave);
|
|
473
1494
|
}
|
|
474
1495
|
}
|
|
475
|
-
}, [
|
|
1496
|
+
}, [historyScopeId, displayItems]);
|
|
476
1497
|
// ---- Derived state ----
|
|
477
1498
|
const messages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
478
1499
|
const ready = true;
|
|
@@ -483,7 +1504,7 @@ pendingPrompt, }) {
|
|
|
483
1504
|
prevMessageCountRef.current = currentCount;
|
|
484
1505
|
onMessagesChange?.(messages);
|
|
485
1506
|
}
|
|
486
|
-
}, [displayItems, onMessagesChange]);
|
|
1507
|
+
}, [displayItems, messages, onMessagesChange]);
|
|
487
1508
|
const padding = compact ? 2 : 3;
|
|
488
1509
|
// Derive approval config from protocol for built-in tool approval support
|
|
489
1510
|
const approvalConfig = useMemo(() => {
|
|
@@ -496,7 +1517,7 @@ pendingPrompt, }) {
|
|
|
496
1517
|
}, [protocol?.configEndpoint, protocol?.authToken]);
|
|
497
1518
|
const defaultAvatarConfig = {
|
|
498
1519
|
userAvatar: _jsx(PersonIcon, { size: 16 }),
|
|
499
|
-
assistantAvatar: _jsx(AiAgentIcon, { size: 16 }),
|
|
1520
|
+
assistantAvatar: _jsx(AiAgentIcon, { size: 16, color: assistantIconColor }),
|
|
500
1521
|
showAvatars: true,
|
|
501
1522
|
avatarSize: 32,
|
|
502
1523
|
userAvatarBg: 'neutral.muted',
|
|
@@ -517,6 +1538,33 @@ pendingPrompt, }) {
|
|
|
517
1538
|
unsubscribeRef.current = adapter.subscribe((event) => {
|
|
518
1539
|
switch (event.type) {
|
|
519
1540
|
case 'message':
|
|
1541
|
+
if (event.usage) {
|
|
1542
|
+
const timestampMs = event.timestamp instanceof Date
|
|
1543
|
+
? event.timestamp.getTime()
|
|
1544
|
+
: Date.now();
|
|
1545
|
+
const promptTokens = Math.max(0, event.usage.promptTokens ?? 0);
|
|
1546
|
+
const completionTokens = Math.max(0, event.usage.completionTokens ?? 0);
|
|
1547
|
+
const totalTokens = Math.max(promptTokens + completionTokens, event.usage.totalTokens ?? 0);
|
|
1548
|
+
const runtimeState = agentRuntimeStore.getState();
|
|
1549
|
+
runtimeState.appendLocalTokenTurn({
|
|
1550
|
+
serviceName: monitoringServiceName,
|
|
1551
|
+
agentId: protocol?.agentId,
|
|
1552
|
+
timestampMs,
|
|
1553
|
+
promptTokens,
|
|
1554
|
+
completionTokens,
|
|
1555
|
+
totalTokens,
|
|
1556
|
+
});
|
|
1557
|
+
const liveCumulativeUsd = runtimeState.costUsage?.cumulativeCostUsd;
|
|
1558
|
+
if (typeof liveCumulativeUsd === 'number' &&
|
|
1559
|
+
Number.isFinite(liveCumulativeUsd)) {
|
|
1560
|
+
runtimeState.upsertLocalCostPoint({
|
|
1561
|
+
serviceName: monitoringServiceName,
|
|
1562
|
+
agentId: protocol?.agentId,
|
|
1563
|
+
timestampMs,
|
|
1564
|
+
cumulativeUsd: Math.max(0, liveCumulativeUsd),
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
520
1568
|
if (suppressAssistantTextForToolOnlyRef.current) {
|
|
521
1569
|
const suppressedMessageId = currentAssistantMessageRef.current?.id;
|
|
522
1570
|
if (suppressedMessageId) {
|
|
@@ -635,6 +1683,12 @@ pendingPrompt, }) {
|
|
|
635
1683
|
};
|
|
636
1684
|
toolCallsRef.current.set(toolCallId, toolCallMsg);
|
|
637
1685
|
setDisplayItems(prev => [...prev, toolCallMsg]);
|
|
1686
|
+
// Fire pre-hook for new tool calls
|
|
1687
|
+
onToolCallStartRef.current?.({
|
|
1688
|
+
toolName,
|
|
1689
|
+
toolCallId,
|
|
1690
|
+
args,
|
|
1691
|
+
});
|
|
638
1692
|
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
639
1693
|
const toolHandler = frontendTool?.handler;
|
|
640
1694
|
// Only execute when we have actual args. AG-UI emits an
|
|
@@ -714,6 +1768,15 @@ pendingPrompt, }) {
|
|
|
714
1768
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
715
1769
|
? updatedToolCall
|
|
716
1770
|
: item));
|
|
1771
|
+
// Fire post-hook for tool results
|
|
1772
|
+
onToolCallCompleteRef.current?.({
|
|
1773
|
+
toolName: existingToolCall.toolName,
|
|
1774
|
+
toolCallId,
|
|
1775
|
+
args: existingToolCall.args,
|
|
1776
|
+
result: event.toolResult.result,
|
|
1777
|
+
status: updatedToolCall.status,
|
|
1778
|
+
error: event.toolResult.error,
|
|
1779
|
+
});
|
|
717
1780
|
}
|
|
718
1781
|
}
|
|
719
1782
|
}
|
|
@@ -779,6 +1842,7 @@ pendingPrompt, }) {
|
|
|
779
1842
|
pendingToolExecutionsRef.current = 0;
|
|
780
1843
|
setIsLoading(false);
|
|
781
1844
|
setIsStreaming(false);
|
|
1845
|
+
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
782
1846
|
break;
|
|
783
1847
|
case 'error':
|
|
784
1848
|
console.error('[ChatBase] Protocol error:', event.error);
|
|
@@ -813,6 +1877,7 @@ pendingPrompt, }) {
|
|
|
813
1877
|
pendingToolExecutionsRef.current = 0;
|
|
814
1878
|
setIsLoading(false);
|
|
815
1879
|
setIsStreaming(false);
|
|
1880
|
+
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
816
1881
|
break;
|
|
817
1882
|
}
|
|
818
1883
|
});
|
|
@@ -879,6 +1944,14 @@ pendingPrompt, }) {
|
|
|
879
1944
|
}
|
|
880
1945
|
// ---- Auto-scroll to bottom ----
|
|
881
1946
|
useEffect(() => {
|
|
1947
|
+
const container = messagesContainerRef.current;
|
|
1948
|
+
if (container) {
|
|
1949
|
+
container.scrollTo({
|
|
1950
|
+
top: container.scrollHeight,
|
|
1951
|
+
behavior: 'smooth',
|
|
1952
|
+
});
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
882
1955
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
883
1956
|
}, [displayItems]);
|
|
884
1957
|
// ========================================================================
|
|
@@ -976,7 +2049,7 @@ pendingPrompt, }) {
|
|
|
976
2049
|
description: tool.description,
|
|
977
2050
|
parameters: tool.parameters || { type: 'object', properties: {} },
|
|
978
2051
|
}));
|
|
979
|
-
console.
|
|
2052
|
+
console.warn('[ChatBase] frontendTools count:', frontendTools?.length ?? 0, 'toolsForRequest:', toolsForRequest.map(t => t.name));
|
|
980
2053
|
const enabledMcpToolNames = getEnabledMcpToolNames();
|
|
981
2054
|
const enabledSkillIds = getEnabledSkillIds();
|
|
982
2055
|
await adapterRef.current.sendMessage(userMessage, {
|
|
@@ -1007,6 +2080,7 @@ pendingPrompt, }) {
|
|
|
1007
2080
|
if (!adapterRef.current) {
|
|
1008
2081
|
setIsLoading(false);
|
|
1009
2082
|
setIsStreaming(false);
|
|
2083
|
+
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
1010
2084
|
}
|
|
1011
2085
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
1012
2086
|
currentAssistantMessageRef.current = null;
|
|
@@ -1020,6 +2094,7 @@ pendingPrompt, }) {
|
|
|
1020
2094
|
frontendTools,
|
|
1021
2095
|
useStoreMode,
|
|
1022
2096
|
onSendMessage,
|
|
2097
|
+
activeAgentId,
|
|
1023
2098
|
enableStreaming,
|
|
1024
2099
|
getEnabledMcpToolNames,
|
|
1025
2100
|
getEnabledSkillIds,
|
|
@@ -1048,6 +2123,7 @@ pendingPrompt, }) {
|
|
|
1048
2123
|
adapterReady,
|
|
1049
2124
|
handleSend,
|
|
1050
2125
|
onSendMessage,
|
|
2126
|
+
applyServerApprovalDecision,
|
|
1051
2127
|
]);
|
|
1052
2128
|
// ---- handleStop ----
|
|
1053
2129
|
const handleStop = useCallback(() => {
|
|
@@ -1102,22 +2178,20 @@ pendingPrompt, }) {
|
|
|
1102
2178
|
pendingToolExecutionsRef.current = 0;
|
|
1103
2179
|
setIsLoading(false);
|
|
1104
2180
|
setIsStreaming(false);
|
|
2181
|
+
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
1105
2182
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
1106
2183
|
currentAssistantMessageRef.current = null;
|
|
1107
2184
|
// Also interrupt any code running in the sandbox (best-effort).
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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(() => { });
|
|
2185
|
+
sandboxStatusQuery.interrupt();
|
|
2186
|
+
// Interrupt the connected notebook kernel as well (best-effort),
|
|
2187
|
+
// matching the toolbar's stop/interrupt behavior.
|
|
2188
|
+
if (kernel && kernel.status === 'busy') {
|
|
2189
|
+
void kernel.interrupt().catch(() => { });
|
|
1118
2190
|
}
|
|
1119
2191
|
}, [
|
|
2192
|
+
kernel,
|
|
1120
2193
|
useStoreMode,
|
|
2194
|
+
activeAgentId,
|
|
1121
2195
|
protocol?.configEndpoint,
|
|
1122
2196
|
protocol?.authToken,
|
|
1123
2197
|
protocol?.agentId,
|
|
@@ -1131,8 +2205,8 @@ pendingPrompt, }) {
|
|
|
1131
2205
|
threadIdRef.current = generateMessageId();
|
|
1132
2206
|
if (useStoreMode)
|
|
1133
2207
|
clearStoreMessages();
|
|
1134
|
-
if (
|
|
1135
|
-
useConversationStore.getState().clearMessages(
|
|
2208
|
+
if (historyScopeId)
|
|
2209
|
+
useConversationStore.getState().clearMessages(historyScopeId);
|
|
1136
2210
|
onNewChat?.();
|
|
1137
2211
|
headerButtons?.onNewChat?.();
|
|
1138
2212
|
}, [clearStoreMessages, onNewChat, headerButtons, useStoreMode, runtimeId]);
|
|
@@ -1143,29 +2217,12 @@ pendingPrompt, }) {
|
|
|
1143
2217
|
toolCallsRef.current.clear();
|
|
1144
2218
|
if (useStoreMode)
|
|
1145
2219
|
clearStoreMessages();
|
|
1146
|
-
if (
|
|
1147
|
-
useConversationStore.getState().clearMessages(
|
|
2220
|
+
if (historyScopeId)
|
|
2221
|
+
useConversationStore.getState().clearMessages(historyScopeId);
|
|
1148
2222
|
onClear?.();
|
|
1149
2223
|
headerButtons?.onClear?.();
|
|
1150
2224
|
}
|
|
1151
2225
|
}, [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
2226
|
// ---- HITL respond handler (passed to MessageList) ----
|
|
1170
2227
|
const handleRespond = useCallback(async (toolCallId, result) => {
|
|
1171
2228
|
const existingToolCall = toolCallsRef.current.get(toolCallId);
|
|
@@ -1174,28 +2231,85 @@ pendingPrompt, }) {
|
|
|
1174
2231
|
existingToolCall.status === 'inProgress')) {
|
|
1175
2232
|
const isApprovalDecision = !!result &&
|
|
1176
2233
|
typeof result === 'object' &&
|
|
1177
|
-
result.type ===
|
|
2234
|
+
result.type ===
|
|
2235
|
+
'tool-approval-decision' &&
|
|
1178
2236
|
typeof result.approved === 'boolean';
|
|
1179
2237
|
if (isApprovalDecision && adapterRef.current) {
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
2238
|
+
const resultRecord = result;
|
|
2239
|
+
const approved = Boolean(resultRecord.approved);
|
|
2240
|
+
const fromServerEcho = resultRecord._fromServerEcho === true;
|
|
2241
|
+
const alreadyDispatched = resultRecord._alreadyDispatched === true;
|
|
2242
|
+
const requiresClientContinuation = existingToolCall.status === 'inProgress';
|
|
2243
|
+
const rawToolName = typeof resultRecord.toolName === 'string'
|
|
2244
|
+
? resultRecord.toolName
|
|
2245
|
+
: existingToolCall.toolName;
|
|
2246
|
+
const isMcpApprovalTool = rawToolName.includes('__');
|
|
2247
|
+
// When the user approves a tool call inline, immediately reflect that
|
|
2248
|
+
// approval in the tools dropdown so the toggle switches to "On".
|
|
2249
|
+
// Tool names follow the convention "serverId__toolName" (MCP) or are
|
|
2250
|
+
// bare skill names. We extract the server prefix for MCP tools.
|
|
2251
|
+
if (approved) {
|
|
2252
|
+
const sep = rawToolName.indexOf('__');
|
|
2253
|
+
if (sep !== -1) {
|
|
2254
|
+
const serverId = rawToolName.slice(0, sep);
|
|
2255
|
+
const toolName = rawToolName.slice(sep + 2);
|
|
2256
|
+
setApprovedMcpTools(prev => {
|
|
2257
|
+
const newMap = new Map(prev);
|
|
2258
|
+
const tools = new Set(prev.get(serverId) ?? []);
|
|
2259
|
+
tools.add(toolName);
|
|
2260
|
+
newMap.set(serverId, tools);
|
|
2261
|
+
return newMap;
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
1193
2265
|
try {
|
|
1194
2266
|
const approvalId = typeof result === 'object' &&
|
|
1195
2267
|
result !== null &&
|
|
1196
2268
|
typeof result.approvalId === 'string'
|
|
1197
2269
|
? result.approvalId
|
|
1198
2270
|
: undefined;
|
|
2271
|
+
// Match AgentToolApprovalsExample semantics: first click sends only
|
|
2272
|
+
// a websocket decision; continuation waits for server echo.
|
|
2273
|
+
// Deferred pending-approval calls (`status === 'inProgress'`) still
|
|
2274
|
+
// require an explicit sendToolResult continuation from the client,
|
|
2275
|
+
// so do not return early in that mode.
|
|
2276
|
+
if (!fromServerEcho && !requiresClientContinuation) {
|
|
2277
|
+
if (approvalId) {
|
|
2278
|
+
if (approved) {
|
|
2279
|
+
await onApproveApproval?.(approvalId, undefined, toolCallId);
|
|
2280
|
+
}
|
|
2281
|
+
else {
|
|
2282
|
+
await onRejectApproval?.(approvalId, undefined, toolCallId);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (approvalId && !alreadyDispatched) {
|
|
2288
|
+
if (approved) {
|
|
2289
|
+
await onApproveApproval?.(approvalId, undefined, toolCallId);
|
|
2290
|
+
}
|
|
2291
|
+
else {
|
|
2292
|
+
await onRejectApproval?.(approvalId, undefined, toolCallId);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
// MCP approvals are unblocked server-side by the websocket decision
|
|
2296
|
+
// and continue on the same stream. Avoid sending an extra
|
|
2297
|
+
// sendToolResult continuation, which can trigger duplicate approvals.
|
|
2298
|
+
if (isMcpApprovalTool && !requiresClientContinuation) {
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const updatedToolCall = {
|
|
2302
|
+
...existingToolCall,
|
|
2303
|
+
result,
|
|
2304
|
+
status: approved ? 'complete' : 'error',
|
|
2305
|
+
error: approved ? undefined : 'Tool approval rejected by user',
|
|
2306
|
+
};
|
|
2307
|
+
toolCallsRef.current.set(toolCallId, updatedToolCall);
|
|
2308
|
+
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2309
|
+
? updatedToolCall
|
|
2310
|
+
: item));
|
|
2311
|
+
setIsLoading(true);
|
|
2312
|
+
setIsStreaming(true);
|
|
1199
2313
|
await adapterRef.current.sendToolResult(toolCallId, {
|
|
1200
2314
|
toolCallId,
|
|
1201
2315
|
success: approved,
|
|
@@ -1210,9 +2324,7 @@ pendingPrompt, }) {
|
|
|
1210
2324
|
message: 'Tool call rejected by user.',
|
|
1211
2325
|
...(approvalId ? { approvalId } : {}),
|
|
1212
2326
|
},
|
|
1213
|
-
...(approved
|
|
1214
|
-
? {}
|
|
1215
|
-
: { error: 'Tool approval rejected by user' }),
|
|
2327
|
+
...(approved ? {} : { error: 'Tool approval rejected by user' }),
|
|
1216
2328
|
});
|
|
1217
2329
|
}
|
|
1218
2330
|
catch (err) {
|
|
@@ -1275,7 +2387,8 @@ pendingPrompt, }) {
|
|
|
1275
2387
|
// event will handle it when the run truly completes.
|
|
1276
2388
|
}
|
|
1277
2389
|
}
|
|
1278
|
-
}, [displayItems]);
|
|
2390
|
+
}, [displayItems, onApproveApproval, onRejectApproval]);
|
|
2391
|
+
handleRespondRef.current = handleRespond;
|
|
1279
2392
|
// ---- Suggestion handlers (for EmptyState) ----
|
|
1280
2393
|
const handleSuggestionSubmit = useCallback((suggestion) => {
|
|
1281
2394
|
void handleSend(suggestion.message);
|
|
@@ -1284,6 +2397,78 @@ pendingPrompt, }) {
|
|
|
1284
2397
|
setInput(message);
|
|
1285
2398
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
1286
2399
|
}, []);
|
|
2400
|
+
// Banner approvals dispatch the decision immediately; the continuation is
|
|
2401
|
+
// resumed when the runtime websocket echoes approved/rejected.
|
|
2402
|
+
const handleBannerApprove = useCallback(async (approvalId, note) => {
|
|
2403
|
+
// Decision dispatch happens here; continuation is resumed on server echo.
|
|
2404
|
+
await onApproveApproval?.(approvalId, note);
|
|
2405
|
+
}, [onApproveApproval]);
|
|
2406
|
+
const handleBannerReject = useCallback(async (approvalId, note) => {
|
|
2407
|
+
// Decision dispatch happens here; continuation is resumed on server echo.
|
|
2408
|
+
await onRejectApproval?.(approvalId, note);
|
|
2409
|
+
}, [onRejectApproval]);
|
|
2410
|
+
// ---- Compute data for InputToolbar ----
|
|
2411
|
+
// Merge real-time WebSocket MCP status into the cached config data so the
|
|
2412
|
+
// dropdown reflects live availability even when the config query was cached
|
|
2413
|
+
// before the MCP servers finished starting.
|
|
2414
|
+
const configMcpServers = (configQuery.data?.mcpServers || []).filter(server => !mcpServers || isServerSelected(server));
|
|
2415
|
+
const filteredMcpServers = useMemo(() => {
|
|
2416
|
+
const merged = configMcpServers.map(server => {
|
|
2417
|
+
const wsServer = effectiveMcpStatusData?.servers?.find(s => s.id === server.id);
|
|
2418
|
+
if (wsServer && wsServer.status === 'started') {
|
|
2419
|
+
const updates = {};
|
|
2420
|
+
if (!server.isAvailable) {
|
|
2421
|
+
updates.isAvailable = true;
|
|
2422
|
+
}
|
|
2423
|
+
// Always prefer WS-discovered tools over cached config data.
|
|
2424
|
+
// The config query may have been fetched before MCP servers
|
|
2425
|
+
// finished starting, leaving tools empty or stale.
|
|
2426
|
+
if (wsServer.tools && wsServer.tools.length > 0) {
|
|
2427
|
+
updates.tools = wsServer.tools.map(t => ({
|
|
2428
|
+
name: t.name,
|
|
2429
|
+
description: t.description || '',
|
|
2430
|
+
enabled: t.enabled ?? true,
|
|
2431
|
+
}));
|
|
2432
|
+
}
|
|
2433
|
+
if (Object.keys(updates).length > 0) {
|
|
2434
|
+
return { ...server, ...updates };
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return server;
|
|
2438
|
+
});
|
|
2439
|
+
// Include WS-only servers that are started but missing from the config
|
|
2440
|
+
// query (e.g. config was fetched before the MCP server finished starting).
|
|
2441
|
+
const configIds = new Set(configMcpServers.map(s => s.id));
|
|
2442
|
+
for (const wsServer of effectiveMcpStatusData?.servers ?? []) {
|
|
2443
|
+
if (wsServer.status === 'started' &&
|
|
2444
|
+
!configIds.has(wsServer.id) &&
|
|
2445
|
+
wsServer.tools &&
|
|
2446
|
+
wsServer.tools.length > 0) {
|
|
2447
|
+
const selected = !mcpServers || mcpServers.some(s => s.id === wsServer.id);
|
|
2448
|
+
if (selected) {
|
|
2449
|
+
merged.push({
|
|
2450
|
+
id: wsServer.id,
|
|
2451
|
+
name: wsServer.id,
|
|
2452
|
+
description: '',
|
|
2453
|
+
url: '',
|
|
2454
|
+
enabled: true,
|
|
2455
|
+
tools: wsServer.tools.map(t => ({
|
|
2456
|
+
name: t.name,
|
|
2457
|
+
description: t.description || '',
|
|
2458
|
+
enabled: t.enabled ?? true,
|
|
2459
|
+
})),
|
|
2460
|
+
args: [],
|
|
2461
|
+
requiredEnvVars: [],
|
|
2462
|
+
isAvailable: true,
|
|
2463
|
+
transport: 'stdio',
|
|
2464
|
+
isConfig: false,
|
|
2465
|
+
isRunning: true,
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return merged;
|
|
2471
|
+
}, [configMcpServers, effectiveMcpStatusData, mcpServers]);
|
|
1287
2472
|
// ---- Not ready ----
|
|
1288
2473
|
if (!ready) {
|
|
1289
2474
|
return (_jsx(Box, { className: className, sx: {
|
|
@@ -1306,8 +2491,10 @@ pendingPrompt, }) {
|
|
|
1306
2491
|
const indicatorApiBase = protocol?.configEndpoint
|
|
1307
2492
|
? protocol.configEndpoint.replace(/\/api\/v1\/(config|configure)\/?$/, '')
|
|
1308
2493
|
: undefined;
|
|
1309
|
-
|
|
1310
|
-
|
|
2494
|
+
const connectionConfirmed = !protocol ||
|
|
2495
|
+
protocol.enableConfigQuery === false ||
|
|
2496
|
+
!!configQuery.data ||
|
|
2497
|
+
!!skillsQuery.data;
|
|
1311
2498
|
// ========================================================================
|
|
1312
2499
|
// Render
|
|
1313
2500
|
// ========================================================================
|
|
@@ -1315,12 +2502,16 @@ pendingPrompt, }) {
|
|
|
1315
2502
|
display: 'flex',
|
|
1316
2503
|
flexDirection: 'column',
|
|
1317
2504
|
height: '100%',
|
|
2505
|
+
maxHeight: '100%',
|
|
2506
|
+
minHeight: 0,
|
|
1318
2507
|
bg: backgroundColor || 'canvas.default',
|
|
1319
2508
|
borderRadius,
|
|
1320
2509
|
border,
|
|
1321
2510
|
boxShadow,
|
|
1322
2511
|
overflow: 'hidden',
|
|
1323
|
-
}, children: [showHeader && (_jsx(ChatBaseHeader, { title: title, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding,
|
|
2512
|
+
}, children: [showHeader && (_jsx(ChatBaseHeader, { title: title, subtitle: subtitle, brandIcon: brandIcon, headerContent: headerContent, headerActions: headerActions, showInformation: showInformation, onInformationClick: onInformationClick, padding: padding, kernelIndicatorState: kernelIndicatorState, runtimeStatus: sandboxStatusData ?? sandboxStatusQuery.data, kernel: kernel, kernelEnvironmentName: kernelEnvironmentName, kernelCpu: kernelCpu, kernelMemory: kernelMemory, kernelGpu: kernelGpu, headerButtons: headerButtons, messageCount: messages.length, onNewChat: handleNewChat, onClear: handleClear, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange })), showToolApprovalBanner &&
|
|
2513
|
+
pendingApprovals &&
|
|
2514
|
+
pendingApprovals.length > 0 && (_jsx(ToolApprovalBannerSection, { pendingApprovals: pendingApprovals, onApprove: handleBannerApprove, onReject: handleBannerReject })), showErrors && error && (_jsxs(Box, { sx: {
|
|
1324
2515
|
display: 'flex',
|
|
1325
2516
|
alignItems: 'center',
|
|
1326
2517
|
gap: 2,
|
|
@@ -1328,11 +2519,60 @@ pendingPrompt, }) {
|
|
|
1328
2519
|
bg: 'danger.subtle',
|
|
1329
2520
|
borderBottom: '1px solid',
|
|
1330
2521
|
borderColor: 'danger.muted',
|
|
1331
|
-
}, children: [_jsx(AlertIcon, { size: 16 }), _jsx(Text, { sx: { color: 'danger.fg', fontSize: 1 }, children: error.message })] })), _jsx(Box, {
|
|
2522
|
+
}, children: [_jsx(AlertIcon, { size: 16 }), _jsx(Text, { sx: { color: 'danger.fg', fontSize: 1 }, children: error.message })] })), _jsx(Box, { ref: messagesContainerRef, sx: {
|
|
2523
|
+
flex: 1,
|
|
2524
|
+
flexGrow: 1,
|
|
2525
|
+
minHeight: 0,
|
|
2526
|
+
overflow: 'auto',
|
|
2527
|
+
bg: 'canvas.default',
|
|
2528
|
+
}, children: children ? (children) : (_jsx(Box, { sx: {
|
|
1332
2529
|
display: 'flex',
|
|
1333
2530
|
flexDirection: 'column',
|
|
1334
|
-
minHeight:
|
|
2531
|
+
minHeight: 0,
|
|
1335
2532
|
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,
|
|
2533
|
+
}, 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, kernelStatus: liveKernelStatus, 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: effectiveMcpStatusData })), showPoweredBy && _jsx(PoweredByTag, { ...poweredByProps })] }));
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Internal component rendering the top-of-chat approval banner + review dialog.
|
|
2537
|
+
* Extracted so we can keep `ChatBase` focused on chat flow while still owning
|
|
2538
|
+
* the banner UX via the `showToolApprovalBanner` prop.
|
|
2539
|
+
*/
|
|
2540
|
+
function ToolApprovalBannerSection({ pendingApprovals, onApprove, onReject, }) {
|
|
2541
|
+
const [activeApproval, setActiveApproval] = useState(null);
|
|
2542
|
+
// Keep the active approval in sync with the incoming list; if the active
|
|
2543
|
+
// one is no longer pending (resolved), dismiss the dialog.
|
|
2544
|
+
useEffect(() => {
|
|
2545
|
+
if (!activeApproval) {
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (!pendingApprovals.some(a => a.id === activeApproval.id)) {
|
|
2549
|
+
setActiveApproval(null);
|
|
2550
|
+
}
|
|
2551
|
+
}, [pendingApprovals, activeApproval]);
|
|
2552
|
+
return (_jsxs(_Fragment, { children: [_jsx(ToolApprovalBanner, { pendingApprovals: pendingApprovals, onReview: approval => setActiveApproval(approval), onApproveAll: async () => {
|
|
2553
|
+
if (!onApprove)
|
|
2554
|
+
return;
|
|
2555
|
+
for (const approval of pendingApprovals) {
|
|
2556
|
+
await onApprove(approval.id);
|
|
2557
|
+
}
|
|
2558
|
+
} }), _jsx(ToolApprovalDialog, { isOpen: !!activeApproval, toolName: activeApproval?.toolName ?? '', toolDescription: activeApproval?.toolDescription, args: activeApproval?.args ?? {}, onApprove: async () => {
|
|
2559
|
+
if (!activeApproval || !onApprove) {
|
|
2560
|
+
setActiveApproval(null);
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
const result = await onApprove(activeApproval.id);
|
|
2564
|
+
if (result !== false) {
|
|
2565
|
+
setActiveApproval(null);
|
|
2566
|
+
}
|
|
2567
|
+
}, onDeny: async () => {
|
|
2568
|
+
if (!activeApproval || !onReject) {
|
|
2569
|
+
setActiveApproval(null);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
const result = await onReject(activeApproval.id, 'Rejected from tool approval dialog');
|
|
2573
|
+
if (result !== false) {
|
|
2574
|
+
setActiveApproval(null);
|
|
2575
|
+
}
|
|
2576
|
+
}, onClose: () => setActiveApproval(null) })] }));
|
|
1337
2577
|
}
|
|
1338
2578
|
export default ChatBase;
|