@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.
Files changed (299) hide show
  1. package/README.md +182 -1
  2. package/lib/AgentNode.d.ts +3 -0
  3. package/lib/AgentNode.js +676 -0
  4. package/lib/App.js +1 -1
  5. package/lib/agent-node/themeStore.d.ts +3 -0
  6. package/lib/agent-node/themeStore.js +156 -0
  7. package/lib/agent-node-main.d.ts +1 -0
  8. package/lib/agent-node-main.js +14 -0
  9. package/lib/agents/AgentDetails.d.ts +22 -1
  10. package/lib/agents/AgentDetails.js +34 -47
  11. package/lib/api/index.d.ts +0 -1
  12. package/lib/api/index.js +4 -2
  13. package/lib/chat/Chat.d.ts +5 -106
  14. package/lib/chat/Chat.js +20 -14
  15. package/lib/chat/ChatFloating.d.ts +7 -140
  16. package/lib/chat/ChatFloating.js +3 -3
  17. package/lib/chat/ChatPopupStandalone.d.ts +8 -47
  18. package/lib/chat/ChatPopupStandalone.js +3 -3
  19. package/lib/chat/ChatSidebar.d.ts +4 -69
  20. package/lib/chat/ChatSidebar.js +83 -51
  21. package/lib/chat/ChatStandalone.d.ts +4 -54
  22. package/lib/chat/ChatStandalone.js +3 -3
  23. package/lib/chat/base/ChatBase.js +1414 -174
  24. package/lib/chat/display/FloatingBrandButton.js +8 -1
  25. package/lib/chat/header/ChatHeader.d.ts +3 -1
  26. package/lib/chat/header/ChatHeader.js +15 -12
  27. package/lib/chat/header/ChatHeaderBase.d.ts +30 -5
  28. package/lib/chat/header/ChatHeaderBase.js +41 -16
  29. package/lib/chat/indicators/McpStatusIndicator.d.ts +7 -4
  30. package/lib/chat/indicators/McpStatusIndicator.js +7 -32
  31. package/lib/chat/indicators/SandboxStatusIndicator.d.ts +4 -1
  32. package/lib/chat/indicators/SandboxStatusIndicator.js +91 -56
  33. package/lib/chat/indicators/SkillsStatusIndicator.d.ts +7 -0
  34. package/lib/chat/indicators/SkillsStatusIndicator.js +88 -0
  35. package/lib/chat/indicators/index.d.ts +1 -0
  36. package/lib/chat/indicators/index.js +1 -0
  37. package/lib/chat/messages/ChatMessageList.d.ts +1 -1
  38. package/lib/chat/messages/ChatMessageList.js +154 -114
  39. package/lib/chat/messages/ChatMessages.js +6 -2
  40. package/lib/chat/prompt/InputFooter.d.ts +21 -6
  41. package/lib/chat/prompt/InputFooter.js +76 -20
  42. package/lib/chat/prompt/InputPrompt.d.ts +5 -1
  43. package/lib/chat/prompt/InputPrompt.js +4 -4
  44. package/lib/chat/prompt/InputPromptFooter.d.ts +3 -1
  45. package/lib/chat/prompt/InputPromptFooter.js +3 -3
  46. package/lib/chat/prompt/InputPromptLexical.d.ts +3 -1
  47. package/lib/chat/prompt/InputPromptLexical.js +12 -5
  48. package/lib/chat/prompt/InputPromptText.d.ts +3 -1
  49. package/lib/chat/prompt/InputPromptText.js +2 -2
  50. package/lib/chat/tools/ToolApprovalBanner.js +1 -1
  51. package/lib/chat/tools/ToolCallDisplay.d.ts +3 -1
  52. package/lib/chat/tools/ToolCallDisplay.js +2 -2
  53. package/lib/chat/usage/TokenUsageBar.js +20 -2
  54. package/lib/client/AgentRuntimesClientContext.d.ts +53 -0
  55. package/lib/client/AgentRuntimesClientContext.js +55 -0
  56. package/lib/client/AgentsMixin.d.ts +0 -18
  57. package/lib/client/AgentsMixin.js +20 -30
  58. package/lib/client/IAgentRuntimesClient.d.ts +215 -0
  59. package/lib/client/IAgentRuntimesClient.js +5 -0
  60. package/lib/client/SdkAgentRuntimesClient.d.ts +151 -0
  61. package/lib/client/SdkAgentRuntimesClient.js +134 -0
  62. package/lib/client/index.d.ts +4 -1
  63. package/lib/client/index.js +3 -1
  64. package/lib/components/NotificationEventCard.js +5 -1
  65. package/lib/config/AgentConfiguration.d.ts +22 -0
  66. package/lib/config/AgentConfiguration.js +319 -64
  67. package/lib/context/ContextDistribution.d.ts +3 -1
  68. package/lib/context/ContextDistribution.js +8 -27
  69. package/lib/context/ContextInspector.d.ts +3 -1
  70. package/lib/context/ContextInspector.js +19 -67
  71. package/lib/context/ContextPanel.d.ts +3 -1
  72. package/lib/context/ContextPanel.js +104 -64
  73. package/lib/context/ContextUsage.d.ts +3 -1
  74. package/lib/context/ContextUsage.js +3 -3
  75. package/lib/context/CostTracker.d.ts +9 -3
  76. package/lib/context/CostTracker.js +26 -47
  77. package/lib/context/CostUsageChart.d.ts +12 -0
  78. package/lib/context/CostUsageChart.js +378 -0
  79. package/lib/context/GraphFlowChart.d.ts +16 -0
  80. package/lib/context/GraphFlowChart.js +182 -0
  81. package/lib/context/TokenUsageChart.d.ts +8 -1
  82. package/lib/context/TokenUsageChart.js +349 -211
  83. package/lib/context/TurnGraphChart.d.ts +39 -0
  84. package/lib/context/TurnGraphChart.js +538 -0
  85. package/lib/context/otelWsPool.d.ts +20 -0
  86. package/lib/context/otelWsPool.js +69 -0
  87. package/lib/examples/A2UiComponentGalleryExample.d.ts +0 -17
  88. package/lib/examples/A2UiComponentGalleryExample.js +315 -522
  89. package/lib/examples/A2UiContactCardExample.d.ts +0 -18
  90. package/lib/examples/A2UiContactCardExample.js +154 -411
  91. package/lib/examples/A2UiRestaurantExample.d.ts +0 -30
  92. package/lib/examples/A2UiRestaurantExample.js +114 -212
  93. package/lib/examples/A2UiViewerExample.d.ts +0 -18
  94. package/lib/examples/A2UiViewerExample.js +283 -532
  95. package/lib/examples/AgUiBackendToolRenderingExample.js +1 -1
  96. package/lib/examples/AgUiHaikuGenUiExample.d.ts +1 -1
  97. package/lib/examples/AgUiHaikuGenUiExample.js +1 -1
  98. package/lib/examples/AgUiSharedStateExample.js +2 -1
  99. package/lib/examples/AgentCheckpointsExample.js +14 -28
  100. package/lib/examples/AgentCodemodeExample.d.ts +4 -6
  101. package/lib/examples/AgentCodemodeExample.js +603 -169
  102. package/lib/examples/AgentEvalsExample.js +339 -53
  103. package/lib/examples/AgentGuardrailsExample.js +383 -66
  104. package/lib/examples/AgentHooksExample.d.ts +3 -0
  105. package/lib/examples/AgentHooksExample.js +122 -0
  106. package/lib/examples/AgentInferenceProviderExample.d.ts +3 -0
  107. package/lib/examples/AgentInferenceProviderExample.js +329 -0
  108. package/lib/examples/AgentMCPExample.d.ts +3 -0
  109. package/lib/examples/AgentMCPExample.js +481 -0
  110. package/lib/examples/AgentMemoryExample.d.ts +1 -2
  111. package/lib/examples/AgentMemoryExample.js +78 -33
  112. package/lib/examples/AgentMonitoringExample.js +261 -200
  113. package/lib/examples/AgentNotificationsExample.d.ts +1 -2
  114. package/lib/examples/AgentNotificationsExample.js +114 -33
  115. package/lib/examples/AgentOtelExample.js +32 -42
  116. package/lib/examples/AgentOutputsExample.d.ts +11 -6
  117. package/lib/examples/AgentOutputsExample.js +433 -81
  118. package/lib/examples/AgentParametersExample.d.ts +3 -0
  119. package/lib/examples/AgentParametersExample.js +248 -0
  120. package/lib/examples/AgentSandboxExample.d.ts +3 -3
  121. package/lib/examples/AgentSandboxExample.js +74 -45
  122. package/lib/examples/AgentSkillsExample.js +95 -103
  123. package/lib/examples/AgentSubagentsExample.d.ts +14 -0
  124. package/lib/examples/AgentSubagentsExample.js +228 -0
  125. package/lib/examples/AgentToolApprovalsExample.js +49 -561
  126. package/lib/examples/AgentTriggersExample.js +823 -569
  127. package/lib/examples/{AgentspecExample.d.ts → AgentspecsExample.d.ts} +2 -2
  128. package/lib/examples/AgentspecsExample.js +1096 -0
  129. package/lib/examples/ChatCustomExample.js +16 -28
  130. package/lib/examples/ChatExample.js +13 -29
  131. package/lib/examples/CopilotKitLexicalExample.js +2 -1
  132. package/lib/examples/CopilotKitNotebookExample.js +2 -1
  133. package/lib/examples/HomeExample.d.ts +15 -0
  134. package/lib/examples/HomeExample.js +77 -0
  135. package/lib/examples/Lexical2Example.js +4 -2
  136. package/lib/examples/{LexicalExample.d.ts → LexicalAgentExample.d.ts} +4 -4
  137. package/lib/examples/{LexicalExample.js → LexicalAgentExample.js} +66 -17
  138. package/lib/examples/{LexicalSidebarExample.d.ts → LexicalAgentSidebarExample.d.ts} +5 -5
  139. package/lib/examples/LexicalAgentSidebarExample.js +261 -0
  140. package/lib/examples/NotebookAgentExample.d.ts +9 -0
  141. package/lib/examples/NotebookAgentExample.js +192 -0
  142. package/lib/examples/{NotebookSidebarExample.d.ts → NotebookAgentSidebarExample.d.ts} +2 -2
  143. package/lib/examples/NotebookAgentSidebarExample.js +221 -0
  144. package/lib/examples/{DatalayerNotebookExample.d.ts → NotebookCollaborationExample.d.ts} +4 -4
  145. package/lib/examples/{DatalayerNotebookExample.js → NotebookCollaborationExample.js} +3 -3
  146. package/lib/examples/NotebookExample.d.ts +4 -7
  147. package/lib/examples/NotebookExample.js +14 -146
  148. package/lib/examples/components/AuthRequiredView.d.ts +6 -0
  149. package/lib/examples/components/AuthRequiredView.js +33 -0
  150. package/lib/examples/components/ExampleWrapper.d.ts +9 -3
  151. package/lib/examples/components/ExampleWrapper.js +45 -9
  152. package/lib/examples/{ag-ui → components}/haiku/HaikuDisplay.js +1 -1
  153. package/lib/examples/{ag-ui → components}/haiku/InlineHaikuCard.js +1 -1
  154. package/lib/examples/{ag-ui → components}/haiku/index.d.ts +1 -1
  155. package/lib/examples/{ag-ui → components}/haiku/index.js +1 -1
  156. package/lib/examples/components/index.d.ts +3 -0
  157. package/lib/examples/components/index.js +4 -0
  158. package/lib/examples/{ag-ui → components}/weather/index.d.ts +1 -1
  159. package/lib/examples/{ag-ui → components}/weather/index.js +1 -1
  160. package/lib/examples/example-selector.d.ts +17 -4
  161. package/lib/examples/example-selector.js +108 -41
  162. package/lib/examples/index.d.ts +10 -6
  163. package/lib/examples/index.js +10 -6
  164. package/lib/examples/lexical/initial-content.json +6 -6
  165. package/lib/examples/main.js +257 -27
  166. package/lib/examples/utils/a2ui.d.ts +18 -0
  167. package/lib/examples/utils/a2ui.js +69 -0
  168. package/lib/examples/utils/a2uiMarkdownProvider.d.ts +7 -0
  169. package/lib/examples/utils/a2uiMarkdownProvider.js +9 -0
  170. package/lib/examples/utils/agentId.d.ts +18 -0
  171. package/lib/examples/utils/agentId.js +54 -0
  172. package/lib/examples/utils/agents/earthquake-detector.json +11 -11
  173. package/lib/examples/utils/agents/sales-forecaster.json +11 -11
  174. package/lib/examples/utils/agents/social-post-generator.json +11 -11
  175. package/lib/examples/utils/agents/stock-market.json +11 -11
  176. package/lib/examples/utils/examplesStore.js +82 -27
  177. package/lib/examples/utils/useExampleAgentRuntimesUrl.d.ts +5 -0
  178. package/lib/examples/utils/useExampleAgentRuntimesUrl.js +19 -0
  179. package/lib/hooks/index.d.ts +8 -8
  180. package/lib/hooks/index.js +7 -7
  181. package/lib/hooks/useA2A.d.ts +2 -3
  182. package/lib/hooks/useAIAgentsWebSocket.d.ts +43 -4
  183. package/lib/hooks/useAIAgentsWebSocket.js +153 -12
  184. package/lib/hooks/useAcp.d.ts +1 -2
  185. package/lib/hooks/useAgUi.d.ts +1 -1
  186. package/lib/hooks/{useAgents.d.ts → useAgentRuntimes.d.ts} +70 -4
  187. package/lib/hooks/{useAgents.js → useAgentRuntimes.js} +237 -32
  188. package/lib/hooks/useAgentsCatalog.js +1 -1
  189. package/lib/hooks/useAgentsService.d.ts +2 -2
  190. package/lib/hooks/useAgentsService.js +7 -7
  191. package/lib/hooks/useCheckpoints.js +1 -1
  192. package/lib/hooks/useConfig.d.ts +4 -1
  193. package/lib/hooks/useConfig.js +10 -3
  194. package/lib/hooks/useContextSnapshot.d.ts +9 -4
  195. package/lib/hooks/useContextSnapshot.js +9 -37
  196. package/lib/hooks/useMonitoring.js +3 -0
  197. package/lib/hooks/useSandbox.d.ts +20 -8
  198. package/lib/hooks/useSandbox.js +105 -40
  199. package/lib/hooks/useSkills.d.ts +23 -5
  200. package/lib/hooks/useSkills.js +94 -39
  201. package/lib/hooks/useToolApprovals.d.ts +60 -36
  202. package/lib/hooks/useToolApprovals.js +318 -69
  203. package/lib/hooks/useVercelAI.d.ts +1 -1
  204. package/lib/index.d.ts +2 -1
  205. package/lib/index.js +1 -0
  206. package/lib/inference/index.d.ts +0 -1
  207. package/lib/middleware/index.d.ts +0 -1
  208. package/lib/protocols/AGUIAdapter.js +6 -0
  209. package/lib/protocols/VercelAIAdapter.d.ts +7 -0
  210. package/lib/protocols/VercelAIAdapter.js +59 -7
  211. package/lib/specs/agents/agents.d.ts +21 -4
  212. package/lib/specs/agents/agents.js +2879 -316
  213. package/lib/specs/agents/index.js +3 -1
  214. package/lib/specs/benchmarks.d.ts +20 -0
  215. package/lib/specs/benchmarks.js +205 -0
  216. package/lib/specs/envvars.js +27 -20
  217. package/lib/specs/evals.d.ts +10 -9
  218. package/lib/specs/evals.js +128 -88
  219. package/lib/specs/events.d.ts +3 -10
  220. package/lib/specs/events.js +127 -84
  221. package/lib/specs/frontendTools.js +2 -2
  222. package/lib/specs/guardrails.d.ts +0 -7
  223. package/lib/specs/guardrails.js +240 -159
  224. package/lib/specs/mcpServers.js +35 -6
  225. package/lib/specs/memory.d.ts +0 -2
  226. package/lib/specs/memory.js +4 -17
  227. package/lib/specs/models.d.ts +0 -2
  228. package/lib/specs/models.js +20 -15
  229. package/lib/specs/notifications.js +102 -18
  230. package/lib/specs/outputs.js +15 -9
  231. package/lib/specs/personas.d.ts +41 -0
  232. package/lib/specs/personas.js +168 -0
  233. package/lib/specs/skills.d.ts +1 -1
  234. package/lib/specs/skills.js +23 -23
  235. package/lib/specs/teams/index.js +3 -1
  236. package/lib/specs/teams/teams.js +468 -348
  237. package/lib/specs/tools.js +4 -4
  238. package/lib/specs/triggers.js +61 -11
  239. package/lib/stores/agentRuntimeStore.d.ts +208 -0
  240. package/lib/stores/agentRuntimeStore.js +650 -0
  241. package/lib/stores/conversationStore.js +2 -2
  242. package/lib/stores/index.d.ts +1 -1
  243. package/lib/stores/index.js +1 -1
  244. package/lib/tools/adapters/copilotkit/lexicalHooks.d.ts +1 -2
  245. package/lib/tools/adapters/copilotkit/lexicalHooks.js +1 -3
  246. package/lib/tools/adapters/copilotkit/notebookHooks.d.ts +1 -2
  247. package/lib/tools/adapters/copilotkit/notebookHooks.js +1 -3
  248. package/lib/tools/index.d.ts +0 -2
  249. package/lib/tools/index.js +0 -1
  250. package/lib/types/agents-lifecycle.d.ts +18 -0
  251. package/lib/types/agents.d.ts +6 -0
  252. package/lib/types/agentspecs.d.ts +54 -1
  253. package/lib/types/benchmarks.d.ts +43 -0
  254. package/lib/types/benchmarks.js +5 -0
  255. package/lib/types/chat.d.ts +325 -8
  256. package/lib/types/context.d.ts +27 -0
  257. package/lib/types/cost.d.ts +2 -2
  258. package/lib/types/evals.d.ts +26 -17
  259. package/lib/types/index.d.ts +3 -0
  260. package/lib/types/index.js +3 -0
  261. package/lib/types/mcp.d.ts +8 -0
  262. package/lib/types/models.d.ts +2 -2
  263. package/lib/types/personas.d.ts +25 -0
  264. package/lib/types/personas.js +5 -0
  265. package/lib/types/skills.d.ts +43 -1
  266. package/lib/types/stream.d.ts +110 -0
  267. package/lib/types/stream.js +36 -0
  268. package/lib/utils/utils.d.ts +9 -5
  269. package/lib/utils/utils.js +9 -5
  270. package/package.json +19 -11
  271. package/scripts/codegen/__pycache__/generate_agents.cpython-313.pyc +0 -0
  272. package/scripts/codegen/__pycache__/generate_benchmarks.cpython-313.pyc +0 -0
  273. package/scripts/codegen/__pycache__/generate_evals.cpython-313.pyc +0 -0
  274. package/scripts/codegen/__pycache__/generate_events.cpython-313.pyc +0 -0
  275. package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
  276. package/scripts/codegen/generate_agents.py +187 -45
  277. package/scripts/codegen/generate_benchmarks.py +441 -0
  278. package/scripts/codegen/generate_evals.py +94 -16
  279. package/scripts/codegen/generate_events.py +35 -14
  280. package/scripts/codegen/generate_personas.py +319 -0
  281. package/scripts/codegen/generate_skills.py +9 -9
  282. package/scripts/sync-jupyter.sh +26 -7
  283. package/lib/api/tool-approvals.d.ts +0 -62
  284. package/lib/api/tool-approvals.js +0 -145
  285. package/lib/examples/AgentspecExample.js +0 -705
  286. package/lib/examples/LexicalSidebarExample.js +0 -163
  287. package/lib/examples/NotebookSidebarExample.js +0 -119
  288. package/lib/examples/NotebookSimpleExample.d.ts +0 -6
  289. package/lib/examples/NotebookSimpleExample.js +0 -22
  290. package/lib/examples/ag-ui/index.d.ts +0 -10
  291. package/lib/examples/ag-ui/index.js +0 -16
  292. package/lib/hooks/useAgentsRegistry.d.ts +0 -10
  293. package/lib/hooks/useAgentsRegistry.js +0 -20
  294. package/lib/stores/agentsStore.d.ts +0 -123
  295. package/lib/stores/agentsStore.js +0 -270
  296. /package/lib/examples/{ag-ui → components}/haiku/HaikuDisplay.d.ts +0 -0
  297. /package/lib/examples/{ag-ui → components}/haiku/InlineHaikuCard.d.ts +0 -0
  298. /package/lib/examples/{ag-ui → components}/weather/InlineWeatherCard.d.ts +0 -0
  299. /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 = false, showToolsMenu = false, showSkillsMenu = false, codemodeEnabled = false, initialModel, availableModels, mcpServers, initialSkills, className, loadingState, headerActions, chatViewMode, onChatViewModeChange,
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(!runtimeId);
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
- // Skills state
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) && codemodeEnabled && showHeader, protocol?.configEndpoint, protocol?.authToken);
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
- const enabledToolNames = new Set(server.tools.filter(t => t.enabled).map(t => t.name));
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
- newMap.set(server.id, prev.get(server.id));
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.filter(t => t.enabled).map(t => t.name));
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
- // Initialize enabled skills from initialSkills prop
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
- if (initialSkills && initialSkills.length > 0) {
301
- setEnabledSkills(new Set(initialSkills));
1102
+ const wsEnabledMcpTools = parseEnabledMcpToolsByServer(effectiveMcpStatusData);
1103
+ if (!wsEnabledMcpTools) {
1104
+ return;
302
1105
  }
303
- }, [initialSkills]);
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, new Set(allToolNames));
1248
+ newMap.set(serverId, nextTools);
324
1249
  }
325
1250
  else {
326
- newMap.set(serverId, new Set());
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
- setEnabledSkills(prev => {
333
- const newSet = new Set(prev);
334
- if (newSet.has(skillId)) {
335
- newSet.delete(skillId);
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
- newSet.add(skillId);
1278
+ wsDisableSkill(id);
339
1279
  }
340
- return newSet;
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 toggleAllSkills = useCallback((allSkillIds, enable) => {
344
- setEnabledSkills(enable ? new Set(allSkillIds) : new Set());
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 prevRuntimeIdRef = useRef(undefined);
1354
+ const prevHistoryScopeRef = useRef(undefined);
369
1355
  useEffect(() => {
370
- if (runtimeId !== prevRuntimeIdRef.current) {
371
- prevRuntimeIdRef.current = runtimeId;
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 (!runtimeId)
1363
+ if (!historyScopeId)
375
1364
  return;
376
1365
  }
377
- else {
1366
+ if (!historyScopeId)
378
1367
  return;
379
- }
380
1368
  const store = useConversationStore.getState();
381
- if (!store.needsFetch(runtimeId)) {
382
- const storedMessages = store.getMessages(runtimeId);
383
- if (storedMessages.length > 0) {
384
- setDisplayItems(storedMessages);
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
- 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);
394
- store.markFetched(runtimeId);
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
- if (protocol?.agentId && !endpoint.includes('agent_id=')) {
399
- const separator = endpoint.includes('?') ? '&' : '?';
400
- endpoint = `${endpoint}${separator}agent_id=${encodeURIComponent(protocol.agentId)}`;
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 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}`);
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
- 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);
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
- catch (err) {
454
- console.error('[ChatBase] Failed to fetch conversation history:', err);
455
- store.markFetched(runtimeId);
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
- runtimeId,
1479
+ historyScopeId,
462
1480
  historyEndpoint,
463
- historyAuthToken,
464
- protocol?.endpoint,
465
- protocol?.authToken,
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 (runtimeId && displayItems.length > 0) {
1488
+ if (historyScopeId && displayItems.length > 0) {
470
1489
  const messagesToSave = displayItems.filter((item) => !isToolCallMessage(item));
471
1490
  if (messagesToSave.length > 0) {
472
- useConversationStore.getState().setMessages(runtimeId, messagesToSave);
1491
+ useConversationStore
1492
+ .getState()
1493
+ .setMessages(historyScopeId, messagesToSave);
473
1494
  }
474
1495
  }
475
- }, [runtimeId, displayItems]);
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.log('[ChatBase] frontendTools count:', frontendTools?.length ?? 0, 'toolsForRequest:', toolsForRequest.map(t => t.name));
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
- 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(() => { });
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 (runtimeId)
1135
- useConversationStore.getState().clearMessages(runtimeId);
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 (runtimeId)
1147
- useConversationStore.getState().clearMessages(runtimeId);
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 === 'tool-approval-decision' &&
2234
+ result.type ===
2235
+ 'tool-approval-decision' &&
1178
2236
  typeof result.approved === 'boolean';
1179
2237
  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);
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
- // ---- Compute data for InputToolbar ----
1310
- const filteredMcpServers = (configQuery.data?.mcpServers || []).filter(server => !mcpServers || isServerSelected(server));
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, sandboxStatus: sandboxStatus, onSandboxInterrupt: handleSandboxInterrupt, headerButtons: headerButtons, messageCount: messages.length, onNewChat: handleNewChat, onClear: handleClear, chatViewMode: chatViewMode, onChatViewModeChange: onChatViewModeChange })), showErrors && error && (_jsxs(Box, { sx: {
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, { sx: { flex: 1, flexGrow: 1, overflow: 'auto', bg: 'canvas.default' }, children: children ? (children) : (_jsx(Box, { sx: {
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: '100%',
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, agentId: protocol?.agentId })), showPoweredBy && _jsx(PoweredByTag, { ...poweredByProps })] }));
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;