@datalayer/agent-runtimes 1.0.4 → 1.0.5

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