@usecrow/ui 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -201,6 +201,10 @@ interface WidgetConfigResponse {
201
201
  label: string;
202
202
  message: string;
203
203
  }>;
204
+ /** Per-tool consent settings from dashboard (shield icon) */
205
+ toolConsentSettings?: Record<string, {
206
+ requires_consent: boolean;
207
+ }>;
204
208
  }
205
209
 
206
210
  /**
@@ -649,6 +653,10 @@ interface UseChatOptions {
649
653
  welcomeMessage?: string;
650
654
  /** AI model to use for this chat (defaults to DEFAULT_MODEL) */
651
655
  selectedModel?: string;
656
+ /** Per-tool consent settings — when a tool has requires_consent, show Allow/Deny before executing */
657
+ toolConsentSettings?: Record<string, {
658
+ requires_consent: boolean;
659
+ }>;
652
660
  onVerificationStatus?: (isVerified: boolean) => void;
653
661
  onConversationId?: (id: string) => void;
654
662
  onWorkflowEvent?: (event: WorkflowEvent) => void;
@@ -656,7 +664,7 @@ interface UseChatOptions {
656
664
  onToolResult?: (toolName: string, result: Record<string, unknown>) => void;
657
665
  onRestoredConversation?: (conversationId: string) => void;
658
666
  }
659
- declare function useChat({ productId, apiUrl, persistAnonymousConversations, welcomeMessage, selectedModel: initialSelectedModel, onVerificationStatus, onConversationId, onWorkflowEvent, onToolCall, onToolResult, onRestoredConversation, }: UseChatOptions): {
667
+ declare function useChat({ productId, apiUrl, persistAnonymousConversations, welcomeMessage, selectedModel: initialSelectedModel, toolConsentSettings, onVerificationStatus, onConversationId, onWorkflowEvent, onToolCall, onToolResult, onRestoredConversation, }: UseChatOptions): {
660
668
  messages: Message[];
661
669
  isLoading: boolean;
662
670
  activeToolCalls: ToolCall[];
@@ -781,6 +789,10 @@ interface UseWidgetStylesResult {
781
789
  label: string;
782
790
  message: string;
783
791
  }>;
792
+ /** Per-tool consent settings from dashboard */
793
+ toolConsentSettings: Record<string, {
794
+ requires_consent: boolean;
795
+ }>;
784
796
  /** Refetch styles from API */
785
797
  refetch: () => Promise<void>;
786
798
  }
@@ -822,6 +834,10 @@ interface UseCopilotStylesResult {
822
834
  welcomeMessage: string | undefined;
823
835
  /** AI model configured for this product */
824
836
  selectedModel: string | undefined;
837
+ /** Per-tool consent settings from dashboard */
838
+ toolConsentSettings: Record<string, {
839
+ requires_consent: boolean;
840
+ }>;
825
841
  /** Refetch styles from API */
826
842
  refetch: () => Promise<void>;
827
843
  }
@@ -1087,10 +1103,9 @@ interface WidgetHeaderProps {
1087
1103
  onNewChat: () => void;
1088
1104
  onToggleHistory: () => void;
1089
1105
  showMinimize?: boolean;
1090
- isMinimized?: boolean;
1091
1106
  onToggleMinimize?: () => void;
1092
1107
  }
1093
- declare function WidgetHeader({ isVerifiedUser, showConversationList, onNewChat, onToggleHistory, showMinimize, isMinimized, onToggleMinimize, }: WidgetHeaderProps): react_jsx_runtime.JSX.Element;
1108
+ declare function WidgetHeader({ isVerifiedUser, showConversationList, onNewChat, onToggleHistory, showMinimize, onToggleMinimize, }: WidgetHeaderProps): react_jsx_runtime.JSX.Element;
1094
1109
 
1095
1110
  /**
1096
1111
  * CopilotToggleButton - Edge toggle button for floating copilot
package/dist/index.d.ts CHANGED
@@ -201,6 +201,10 @@ interface WidgetConfigResponse {
201
201
  label: string;
202
202
  message: string;
203
203
  }>;
204
+ /** Per-tool consent settings from dashboard (shield icon) */
205
+ toolConsentSettings?: Record<string, {
206
+ requires_consent: boolean;
207
+ }>;
204
208
  }
205
209
 
206
210
  /**
@@ -649,6 +653,10 @@ interface UseChatOptions {
649
653
  welcomeMessage?: string;
650
654
  /** AI model to use for this chat (defaults to DEFAULT_MODEL) */
651
655
  selectedModel?: string;
656
+ /** Per-tool consent settings — when a tool has requires_consent, show Allow/Deny before executing */
657
+ toolConsentSettings?: Record<string, {
658
+ requires_consent: boolean;
659
+ }>;
652
660
  onVerificationStatus?: (isVerified: boolean) => void;
653
661
  onConversationId?: (id: string) => void;
654
662
  onWorkflowEvent?: (event: WorkflowEvent) => void;
@@ -656,7 +664,7 @@ interface UseChatOptions {
656
664
  onToolResult?: (toolName: string, result: Record<string, unknown>) => void;
657
665
  onRestoredConversation?: (conversationId: string) => void;
658
666
  }
659
- declare function useChat({ productId, apiUrl, persistAnonymousConversations, welcomeMessage, selectedModel: initialSelectedModel, onVerificationStatus, onConversationId, onWorkflowEvent, onToolCall, onToolResult, onRestoredConversation, }: UseChatOptions): {
667
+ declare function useChat({ productId, apiUrl, persistAnonymousConversations, welcomeMessage, selectedModel: initialSelectedModel, toolConsentSettings, onVerificationStatus, onConversationId, onWorkflowEvent, onToolCall, onToolResult, onRestoredConversation, }: UseChatOptions): {
660
668
  messages: Message[];
661
669
  isLoading: boolean;
662
670
  activeToolCalls: ToolCall[];
@@ -781,6 +789,10 @@ interface UseWidgetStylesResult {
781
789
  label: string;
782
790
  message: string;
783
791
  }>;
792
+ /** Per-tool consent settings from dashboard */
793
+ toolConsentSettings: Record<string, {
794
+ requires_consent: boolean;
795
+ }>;
784
796
  /** Refetch styles from API */
785
797
  refetch: () => Promise<void>;
786
798
  }
@@ -822,6 +834,10 @@ interface UseCopilotStylesResult {
822
834
  welcomeMessage: string | undefined;
823
835
  /** AI model configured for this product */
824
836
  selectedModel: string | undefined;
837
+ /** Per-tool consent settings from dashboard */
838
+ toolConsentSettings: Record<string, {
839
+ requires_consent: boolean;
840
+ }>;
825
841
  /** Refetch styles from API */
826
842
  refetch: () => Promise<void>;
827
843
  }
@@ -1087,10 +1103,9 @@ interface WidgetHeaderProps {
1087
1103
  onNewChat: () => void;
1088
1104
  onToggleHistory: () => void;
1089
1105
  showMinimize?: boolean;
1090
- isMinimized?: boolean;
1091
1106
  onToggleMinimize?: () => void;
1092
1107
  }
1093
- declare function WidgetHeader({ isVerifiedUser, showConversationList, onNewChat, onToggleHistory, showMinimize, isMinimized, onToggleMinimize, }: WidgetHeaderProps): react_jsx_runtime.JSX.Element;
1108
+ declare function WidgetHeader({ isVerifiedUser, showConversationList, onNewChat, onToggleHistory, showMinimize, onToggleMinimize, }: WidgetHeaderProps): react_jsx_runtime.JSX.Element;
1094
1109
 
1095
1110
  /**
1096
1111
  * CopilotToggleButton - Edge toggle button for floating copilot
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
3
3
  import { DEFAULT_TOOLS, CrowClient } from '@usecrow/client';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { createPortal } from 'react-dom';
6
- import { Square, ArrowUp, ChevronDown, Check, MessageCircle, Plus, History, ChevronUp, Brain, ChevronRight, Loader2, X } from 'lucide-react';
6
+ import { Square, ArrowUp, ChevronDown, Check, MessageCircle, Plus, RotateCcw, History, X, Brain, ChevronRight, Loader2 } from 'lucide-react';
7
7
  import ReactMarkdown from 'react-markdown';
8
8
  import * as TooltipPrimitive from '@radix-ui/react-tooltip';
9
9
 
@@ -44,6 +44,7 @@ function useChat({
44
44
  persistAnonymousConversations,
45
45
  welcomeMessage,
46
46
  selectedModel: initialSelectedModel,
47
+ toolConsentSettings,
47
48
  onVerificationStatus,
48
49
  onConversationId,
49
50
  onWorkflowEvent,
@@ -68,6 +69,8 @@ function useChat({
68
69
  const abortControllerRef = useRef(null);
69
70
  const hasCheckedPersistRef = useRef(false);
70
71
  const streamingToolCallsRef = useRef([]);
72
+ const toolConsentSettingsRef = useRef(toolConsentSettings);
73
+ toolConsentSettingsRef.current = toolConsentSettings;
71
74
  useEffect(() => {
72
75
  if (initialSelectedModel) {
73
76
  setSelectedModel((prev) => prev !== initialSelectedModel ? initialSelectedModel : prev);
@@ -319,26 +322,43 @@ function useChat({
319
322
  }
320
323
  break;
321
324
  case "client_tool_call":
322
- onToolCall?.({
323
- type: "start",
324
- toolName: parsed.tool_name,
325
- arguments: parsed.arguments
326
- });
327
- const clientToolCall = {
328
- id: parsed.tool_call_id || `tool-${Date.now()}`,
329
- name: parsed.tool_name,
330
- displayName: parsed.display_name || void 0,
331
- arguments: parsed.arguments || {},
332
- status: "executing",
333
- timestamp: /* @__PURE__ */ new Date()
334
- };
335
- streamingToolCallsRef.current = [...streamingToolCallsRef.current, clientToolCall];
336
- setActiveToolCalls((prev) => [...prev, clientToolCall]);
337
- pendingClientTools.push({
338
- toolName: parsed.tool_name,
339
- toolCallId: parsed.tool_call_id,
340
- arguments: parsed.arguments
341
- });
325
+ {
326
+ const needsConsent = toolConsentSettingsRef.current?.[parsed.tool_name]?.requires_consent === true;
327
+ onToolCall?.({
328
+ type: "start",
329
+ toolName: parsed.tool_name,
330
+ arguments: parsed.arguments
331
+ });
332
+ if (needsConsent) {
333
+ const consentClientTc = {
334
+ id: parsed.tool_call_id || `tool-${Date.now()}`,
335
+ name: parsed.tool_name,
336
+ displayName: parsed.display_name || void 0,
337
+ arguments: parsed.arguments || {},
338
+ status: "awaiting_consent",
339
+ requiresConsent: true,
340
+ timestamp: /* @__PURE__ */ new Date()
341
+ };
342
+ streamingToolCallsRef.current = [...streamingToolCallsRef.current, consentClientTc];
343
+ setActiveToolCalls((prev) => [...prev, consentClientTc]);
344
+ } else {
345
+ const clientToolCall = {
346
+ id: parsed.tool_call_id || `tool-${Date.now()}`,
347
+ name: parsed.tool_name,
348
+ displayName: parsed.display_name || void 0,
349
+ arguments: parsed.arguments || {},
350
+ status: "executing",
351
+ timestamp: /* @__PURE__ */ new Date()
352
+ };
353
+ streamingToolCallsRef.current = [...streamingToolCallsRef.current, clientToolCall];
354
+ setActiveToolCalls((prev) => [...prev, clientToolCall]);
355
+ pendingClientTools.push({
356
+ toolName: parsed.tool_name,
357
+ toolCallId: parsed.tool_call_id,
358
+ arguments: parsed.arguments
359
+ });
360
+ }
361
+ }
342
362
  break;
343
363
  case "tool_consent_required":
344
364
  onToolCall?.({
@@ -720,21 +740,36 @@ function useChat({
720
740
  break;
721
741
  case "client_tool_call":
722
742
  {
723
- const toolCallEntry = {
724
- id: parsed.tool_call_id || `tool-${Date.now()}`,
725
- name: parsed.tool_name,
726
- displayName: parsed.display_name || void 0,
727
- arguments: parsed.arguments || {},
728
- status: "executing",
729
- timestamp: /* @__PURE__ */ new Date()
730
- };
731
- streamingToolCallsRef.current = [...streamingToolCallsRef.current, toolCallEntry];
732
- setActiveToolCalls((prev) => [...prev, toolCallEntry]);
733
- pendingClientTools.push({
734
- toolName: parsed.tool_name,
735
- toolCallId: parsed.tool_call_id,
736
- arguments: parsed.arguments
737
- });
743
+ const needsConsent2 = toolConsentSettingsRef.current?.[parsed.tool_name]?.requires_consent === true;
744
+ if (needsConsent2) {
745
+ const consentEntry2 = {
746
+ id: parsed.tool_call_id || `tool-${Date.now()}`,
747
+ name: parsed.tool_name,
748
+ displayName: parsed.display_name || void 0,
749
+ arguments: parsed.arguments || {},
750
+ status: "awaiting_consent",
751
+ requiresConsent: true,
752
+ timestamp: /* @__PURE__ */ new Date()
753
+ };
754
+ streamingToolCallsRef.current = [...streamingToolCallsRef.current, consentEntry2];
755
+ setActiveToolCalls((prev) => [...prev, consentEntry2]);
756
+ } else {
757
+ const toolCallEntry = {
758
+ id: parsed.tool_call_id || `tool-${Date.now()}`,
759
+ name: parsed.tool_name,
760
+ displayName: parsed.display_name || void 0,
761
+ arguments: parsed.arguments || {},
762
+ status: "executing",
763
+ timestamp: /* @__PURE__ */ new Date()
764
+ };
765
+ streamingToolCallsRef.current = [...streamingToolCallsRef.current, toolCallEntry];
766
+ setActiveToolCalls((prev) => [...prev, toolCallEntry]);
767
+ pendingClientTools.push({
768
+ toolName: parsed.tool_name,
769
+ toolCallId: parsed.tool_call_id,
770
+ arguments: parsed.arguments
771
+ });
772
+ }
738
773
  }
739
774
  break;
740
775
  case "tool_consent_required":
@@ -1501,6 +1536,9 @@ function useWidgetStyles({
1501
1536
  const [initialSuggestions, setInitialSuggestions] = useState(
1502
1537
  styleCache.get(key)?.initialSuggestions || []
1503
1538
  );
1539
+ const [toolConsentSettings, setToolConsentSettings] = useState(
1540
+ styleCache.get(key)?.toolConsentSettings || {}
1541
+ );
1504
1542
  const [agentName, setAgentName] = useState(
1505
1543
  styleCache.get(key)?.agentName || "Assistant"
1506
1544
  );
@@ -1541,6 +1579,7 @@ function useWidgetStyles({
1541
1579
  setWelcomeMessage(config.welcomeMessage ?? void 0);
1542
1580
  setSelectedModel(config.model ?? void 0);
1543
1581
  setInitialSuggestions(config.initialSuggestions || []);
1582
+ setToolConsentSettings(config.toolConsentSettings || {});
1544
1583
  } catch (err) {
1545
1584
  console.error("[CrowWidget] Failed to fetch styles:", err);
1546
1585
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1579,6 +1618,7 @@ function useWidgetStyles({
1579
1618
  welcomeMessage,
1580
1619
  selectedModel,
1581
1620
  initialSuggestions,
1621
+ toolConsentSettings,
1582
1622
  refetch: fetchStyles
1583
1623
  };
1584
1624
  }
@@ -1614,6 +1654,9 @@ function useCopilotStyles({
1614
1654
  const [selectedModel, setSelectedModel] = useState(
1615
1655
  styleCache.get(key)?.model ?? void 0
1616
1656
  );
1657
+ const [toolConsentSettings, setToolConsentSettings] = useState(
1658
+ styleCache.get(key)?.toolConsentSettings || {}
1659
+ );
1617
1660
  const hasFetchedRef = useRef(false);
1618
1661
  const fetchStyles = async () => {
1619
1662
  if (skip) return;
@@ -1630,6 +1673,7 @@ function useCopilotStyles({
1630
1673
  setPersistAnonymousConversations(config.persistAnonymousConversations ?? true);
1631
1674
  setWelcomeMessage(config.welcomeMessage ?? void 0);
1632
1675
  setSelectedModel(config.model ?? void 0);
1676
+ setToolConsentSettings(config.toolConsentSettings || {});
1633
1677
  } catch (err) {
1634
1678
  console.error("[CrowCopilot] Failed to fetch styles:", err);
1635
1679
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -1667,6 +1711,7 @@ function useCopilotStyles({
1667
1711
  persistAnonymousConversations,
1668
1712
  welcomeMessage,
1669
1713
  selectedModel,
1714
+ toolConsentSettings,
1670
1715
  refetch: fetchStyles
1671
1716
  };
1672
1717
  }
@@ -1911,7 +1956,6 @@ function WidgetHeader({
1911
1956
  onNewChat,
1912
1957
  onToggleHistory,
1913
1958
  showMinimize = false,
1914
- isMinimized = false,
1915
1959
  onToggleMinimize
1916
1960
  }) {
1917
1961
  const { agentName, styles } = useWidgetStyleContext();
@@ -1933,7 +1977,7 @@ function WidgetHeader({
1933
1977
  }
1934
1978
  ) }),
1935
1979
  /* @__PURE__ */ jsxs("div", { className: "crow-flex crow-items-center crow-gap-1", children: [
1936
- /* @__PURE__ */ jsx(
1980
+ isVerifiedUser ? /* @__PURE__ */ jsx(
1937
1981
  "button",
1938
1982
  {
1939
1983
  onClick: onNewChat,
@@ -1942,6 +1986,15 @@ function WidgetHeader({
1942
1986
  title: "New Chat",
1943
1987
  children: /* @__PURE__ */ jsx(Plus, { size: 18, className: "crow-text-gray-700" })
1944
1988
  }
1989
+ ) : /* @__PURE__ */ jsx(
1990
+ "button",
1991
+ {
1992
+ onClick: onNewChat,
1993
+ className: "crow-p-1.5 hover:crow-bg-gray-200 crow-rounded crow-transition-colors",
1994
+ "aria-label": "Restart Chat",
1995
+ title: "Restart Chat",
1996
+ children: /* @__PURE__ */ jsx(RotateCcw, { size: 16, className: "crow-text-gray-700" })
1997
+ }
1945
1998
  ),
1946
1999
  isVerifiedUser && /* @__PURE__ */ jsx(
1947
2000
  "button",
@@ -1957,9 +2010,10 @@ function WidgetHeader({
1957
2010
  "button",
1958
2011
  {
1959
2012
  onClick: onToggleMinimize,
1960
- className: "crow-p-1 hover:crow-bg-gray-200 crow-rounded crow-transition-colors",
1961
- "aria-label": isMinimized ? "Expand" : "Minimize",
1962
- children: isMinimized ? /* @__PURE__ */ jsx(ChevronUp, { size: 18, className: "crow-text-gray-900" }) : /* @__PURE__ */ jsx(ChevronDown, { size: 18, className: "crow-text-gray-900" })
2013
+ className: "crow-p-1.5 hover:crow-bg-gray-200 crow-rounded crow-transition-colors",
2014
+ "aria-label": "Close chat",
2015
+ title: "Close chat",
2016
+ children: /* @__PURE__ */ jsx(X, { size: 18, className: "crow-text-gray-700" })
1963
2017
  }
1964
2018
  )
1965
2019
  ] })
@@ -3322,7 +3376,8 @@ function CrowWidget({
3322
3376
  persistAnonymousConversations,
3323
3377
  welcomeMessage: welcomeMessageFromAPI,
3324
3378
  selectedModel: selectedModelFromAPI,
3325
- initialSuggestions
3379
+ initialSuggestions,
3380
+ toolConsentSettings
3326
3381
  } = useWidgetStyles({
3327
3382
  productId,
3328
3383
  apiUrl,
@@ -3365,6 +3420,7 @@ function CrowWidget({
3365
3420
  persistAnonymousConversations,
3366
3421
  welcomeMessage,
3367
3422
  selectedModel,
3423
+ toolConsentSettings,
3368
3424
  onVerificationStatus: (isVerified) => {
3369
3425
  setIsVerifiedUser(isVerified);
3370
3426
  },
@@ -3667,14 +3723,47 @@ function CrowWidget({
3667
3723
  const handleToolConsent = async (toolCallId, approved) => {
3668
3724
  const toolCall = chat.activeToolCalls.find((tc) => tc.id === toolCallId) || chat.messages.flatMap((m) => m.toolCalls || []).find((tc) => tc.id === toolCallId);
3669
3725
  if (!toolCall) return;
3726
+ const isClientSide = !toolCall.serverSideExecution;
3670
3727
  if (approved) {
3671
3728
  chat.updateToolCallStatus(toolCallId, "executing");
3672
- if (submitToolResultRef.current) {
3673
- await submitToolResultRef.current(
3674
- toolCallId,
3675
- toolCall.name,
3676
- { consent_approved: true, tool_arguments: toolCall.arguments || {} }
3677
- );
3729
+ if (isClientSide) {
3730
+ try {
3731
+ const result = await executeClientToolRef.current?.(
3732
+ toolCall.name,
3733
+ toolCall.arguments || {}
3734
+ );
3735
+ const resultObj = result;
3736
+ const dataObj = resultObj?.data;
3737
+ const wasUserCancelled = dataObj?.declined === true || typeof resultObj?.error === "string" && resultObj.error.includes("cancelled by user") || typeof resultObj?.error === "string" && resultObj.error.includes("declined");
3738
+ if (wasUserCancelled) {
3739
+ console.log("[Crow Widget] Tool was cancelled by user after consent");
3740
+ return;
3741
+ }
3742
+ if (result && submitToolResultRef.current) {
3743
+ await submitToolResultRef.current(
3744
+ toolCallId,
3745
+ toolCall.name,
3746
+ result
3747
+ );
3748
+ }
3749
+ } catch (e) {
3750
+ console.error("[Crow Widget] Tool error after consent:", e);
3751
+ if (submitToolResultRef.current) {
3752
+ await submitToolResultRef.current(
3753
+ toolCallId,
3754
+ toolCall.name,
3755
+ { success: false, error: String(e) }
3756
+ );
3757
+ }
3758
+ }
3759
+ } else {
3760
+ if (submitToolResultRef.current) {
3761
+ await submitToolResultRef.current(
3762
+ toolCallId,
3763
+ toolCall.name,
3764
+ { consent_approved: true, tool_arguments: toolCall.arguments || {} }
3765
+ );
3766
+ }
3678
3767
  }
3679
3768
  } else {
3680
3769
  chat.updateToolCallStatus(toolCallId, "denied");
@@ -3722,7 +3811,9 @@ function CrowWidget({
3722
3811
  isVerifiedUser,
3723
3812
  showConversationList,
3724
3813
  onNewChat: handleNewChat,
3725
- onToggleHistory: handleToggleHistory
3814
+ onToggleHistory: handleToggleHistory,
3815
+ showMinimize: variant === "floating",
3816
+ onToggleMinimize: () => setIsCollapsed(true)
3726
3817
  }
3727
3818
  ),
3728
3819
  /* @__PURE__ */ jsx(AnimatePresence, { children: showConversationList && isVerifiedUser && /* @__PURE__ */ jsx(
@@ -4103,7 +4194,8 @@ function CrowCopilot({
4103
4194
  pageNavigationRoutes,
4104
4195
  persistAnonymousConversations,
4105
4196
  welcomeMessage: welcomeMessageFromAPI,
4106
- selectedModel
4197
+ selectedModel,
4198
+ toolConsentSettings
4107
4199
  } = useCopilotStyles({
4108
4200
  productId,
4109
4201
  apiUrl,
@@ -4294,6 +4386,7 @@ function CrowCopilot({
4294
4386
  persistAnonymousConversations,
4295
4387
  welcomeMessage,
4296
4388
  selectedModel,
4389
+ toolConsentSettings,
4297
4390
  onVerificationStatus: (isVerified) => {
4298
4391
  setIsVerifiedUser(isVerified);
4299
4392
  },
@@ -4492,14 +4585,54 @@ function CrowCopilot({
4492
4585
  const handleToolConsent = async (toolCallId, approved) => {
4493
4586
  const toolCall = chat.activeToolCalls.find((tc) => tc.id === toolCallId) || chat.messages.flatMap((m) => m.toolCalls || []).find((tc) => tc.id === toolCallId);
4494
4587
  if (!toolCall) return;
4588
+ const isClientSide = !toolCall.serverSideExecution;
4495
4589
  if (approved) {
4496
4590
  chat.updateToolCallStatus(toolCallId, "executing");
4497
- if (submitToolResultRef.current) {
4498
- await submitToolResultRef.current(
4499
- toolCallId,
4500
- toolCall.name,
4501
- { consent_approved: true, tool_arguments: toolCall.arguments || {} }
4502
- );
4591
+ if (isClientSide) {
4592
+ try {
4593
+ const result = await executeClientToolRef.current?.(
4594
+ toolCall.name,
4595
+ toolCall.arguments || {}
4596
+ );
4597
+ const resultObj = result;
4598
+ const dataObj = resultObj?.data;
4599
+ const wasUserCancelled = dataObj?.declined === true || typeof resultObj?.error === "string" && resultObj.error.includes("cancelled by user") || typeof resultObj?.error === "string" && resultObj.error.includes("declined");
4600
+ if (wasUserCancelled) {
4601
+ console.log("[Crow Copilot] Tool was cancelled by user after consent");
4602
+ if (submitToolResultRef.current) {
4603
+ await submitToolResultRef.current(
4604
+ toolCallId,
4605
+ toolCall.name,
4606
+ { success: false, cancelled: true, error: "Action was cancelled by the user." }
4607
+ );
4608
+ }
4609
+ return;
4610
+ }
4611
+ if (result && submitToolResultRef.current) {
4612
+ await submitToolResultRef.current(
4613
+ toolCallId,
4614
+ toolCall.name,
4615
+ result
4616
+ );
4617
+ }
4618
+ } catch (e) {
4619
+ console.error("[Crow Copilot] Tool error after consent:", e);
4620
+ if (submitToolResultRef.current) {
4621
+ await submitToolResultRef.current(
4622
+ toolCallId,
4623
+ toolCall.name,
4624
+ { success: false, error: String(e) }
4625
+ );
4626
+ }
4627
+ }
4628
+ } else {
4629
+ if (submitToolResultRef.current) {
4630
+ await submitToolResultRef.current(
4631
+ toolCallId,
4632
+ toolCall.name,
4633
+ { consent_approved: true, tool_arguments: toolCall.arguments || {} }
4634
+ );
4635
+ }
4503
4636
  }
4504
4637
  } else {
4505
4638
  chat.updateToolCallStatus(toolCallId, "denied");