From 12b48e255ced255449eab64602b65d1aae207a41 Mon Sep 17 00:00:00 2001 From: Kumar Abhirup Date: Wed, 8 Apr 2026 00:27:25 -0700 Subject: [PATCH 1/2] fix(web): surface Composio execution details in chat Keep Dench Integration search and execution metadata visible in chat so users can understand the selected tool, router session, recovery path, and failure details after the new gateway contract. Update the chain-of-thought and persisted message rendering paths together so streamed and stored tool output stay consistent. Made-with: Cursor --- .../app/components/chain-of-thought.test.tsx | 140 +++++++++++++++++- apps/web/app/components/chain-of-thought.tsx | 75 ++++++++-- apps/web/app/components/chat-message.test.tsx | 78 ++++++++++ apps/web/app/components/chat-message.tsx | 35 +++-- 4 files changed, 304 insertions(+), 24 deletions(-) diff --git a/apps/web/app/components/chain-of-thought.test.tsx b/apps/web/app/components/chain-of-thought.test.tsx index 12f7d4ec1f3f..f2193c8a8f6d 100644 --- a/apps/web/app/components/chain-of-thought.test.tsx +++ b/apps/web/app/components/chain-of-thought.test.tsx @@ -107,15 +107,17 @@ describe("ChainOfThought integration steps", () => { toolCallId: "tool-call-1", status: "done", args: { - app: "stripe", - tool_name: "STRIPE_LIST_SUBSCRIPTIONS", - account: "acct_primary", + execution_ref: "exec_stripe_123", arguments: { limit: 100, starting_after: "sub_prev", }, }, output: { + tool_slug: "STRIPE_LIST_SUBSCRIPTIONS", + toolkit: "stripe", + account: "acct_primary", + tool_router_session_id: "trs_123", has_more: true, next_cursor: "sub_next", data: [{ id: "sub_123" }], @@ -132,9 +134,141 @@ describe("ChainOfThought integration steps", () => { expect(screen.getByText(/stripe \/ STRIPE_LIST_SUBSCRIPTIONS/)).toBeTruthy(); expect(screen.getAllByText(/Account:/i).length).toBeGreaterThan(0); expect(screen.getByText("acct_primary")).toBeTruthy(); + expect(screen.getByText(/Session:/i)).toBeTruthy(); + expect(screen.getByText("trs_123")).toBeTruthy(); expect(screen.getByText(/Pagination:/i)).toBeTruthy(); expect(screen.getByText(/has_more: true \| next_cursor: sub_next/)).toBeTruthy(); expect(screen.getByText(/Result:/i)).toBeTruthy(); expect(screen.getByText("1 result")).toBeTruthy(); }); + + it("shows recovery details for an auto-healed Dench Integration call", () => { + const parts: ChainPart[] = [ + { + kind: "tool", + toolName: "composio_call_tool", + toolCallId: "tool-call-recovered", + status: "done", + args: { + execution_ref: "exec_youtube_old", + arguments: { + maxResults: 50, + part: "snippet,contentDetails", + }, + }, + output: { + tool_slug: "YOUTUBE_LIST_USER_SUBSCRIPTIONS", + toolkit: "youtube", + account: "ca_youtube_1", + tool_router_session_id: "trs_youtube_1", + data: [{ id: "sub_123" }], + recovery: { + recovered: true, + recovered_via: "auto_bind_single_active_account", + retried_with_account: "ca_youtube_1", + refreshed_execution_ref: "exec_youtube_refreshed", + }, + }, + }, + ]; + + render(); + fireEvent.click(screen.getByRole("button", { name: /thought/i })); + + expect(screen.getByText(/Recovery:/i)).toBeTruthy(); + expect( + screen.getByText( + /auto_bind_single_active_account \| account: ca_youtube_1 \| refreshed execution_ref returned/, + ), + ).toBeTruthy(); + }); + + it("keeps Dench Integration call details visible when the tool fails", () => { + const parts: ChainPart[] = [ + { + kind: "tool", + toolName: "composio_call_tool", + toolCallId: "tool-call-error", + status: "error", + args: { + execution_ref: "exec_posthog_1", + arguments: { + project_id: "proj_123", + query: "SELECT * FROM events", + }, + }, + output: { + tool_slug: "POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS", + toolkit: "posthog", + account: "ca_obBGycWy7ChR", + tool_router_session_id: "trs_posthog_123", + error: "Dench Integration execution failed (HTTP 400).", + }, + }, + ]; + + render(); + fireEvent.click(screen.getByRole("button", { name: /thought/i })); + + expect(screen.getByText(denchIntegrationsBrand.callLabel)).toBeTruthy(); + expect( + screen.getByText(/Dench Integration execution failed \(HTTP 400\)/i), + ).toBeTruthy(); + expect(screen.getByText(/Tool:/i)).toBeTruthy(); + expect( + screen.getByText(/posthog \/ POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS/), + ).toBeTruthy(); + expect(screen.getAllByText(/Account:/i).length).toBeGreaterThan(0); + expect(screen.getByText("ca_obBGycWy7ChR")).toBeTruthy(); + expect(screen.getByText(/Args:/i)).toBeTruthy(); + expect(screen.getByText(/project_id: proj_123 \| query: SELECT \* FROM events/)).toBeTruthy(); + expect(screen.getByText(/execution_ref: exec_posthog_1/)).toBeTruthy(); + }); + + it("keeps Dench Integrations search details visible when the tool fails", () => { + const parts: ChainPart[] = [ + { + kind: "tool", + toolName: "composio_search_tools", + toolCallId: "tool-search-error", + status: "error", + args: { + query: "list PostHog projects", + }, + errorText: "Gateway search succeeded but execution context was rejected.", + output: { + search_session_id: "trs_posthog_123", + results: [ + { + tool: "POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS", + }, + ], + recommended_result: { + account_candidates: [ + { display_label: "PostHog account" }, + ], + input_schema: { + type: "object", + required: ["project_id"], + properties: { + project_id: { type: "number" }, + }, + }, + }, + }, + }, + ]; + + render(); + fireEvent.click(screen.getByRole("button", { name: /thought/i })); + + expect(screen.getByText(denchIntegrationsBrand.searchLabel)).toBeTruthy(); + expect( + screen.getByText(/Gateway search succeeded but execution context was rejected./), + ).toBeTruthy(); + expect(screen.getByText(/Top matches:/i)).toBeTruthy(); + expect(screen.getByText(/POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS/)).toBeTruthy(); + expect(screen.getByText(/Session:/i)).toBeTruthy(); + expect(screen.getByText("trs_posthog_123")).toBeTruthy(); + }); }); diff --git a/apps/web/app/components/chain-of-thought.tsx b/apps/web/app/components/chain-of-thought.tsx index 84b84c1a6961..b1d1878a2c74 100644 --- a/apps/web/app/components/chain-of-thought.tsx +++ b/apps/web/app/components/chain-of-thought.tsx @@ -284,7 +284,12 @@ function buildComposioCallCardData( args?: Record, output?: Record, ) { - const toolName = readStringValue(args?.tool_name); + const execution = asRecordValue(output?.execution); + const recovery = asRecordValue(output?.recovery); + const toolName = + readStringValue(args?.tool_name) ?? + readStringValue(output?.tool_slug) ?? + readStringValue(execution?.tool_name); if (!toolName) {return null;} const toolArgs = asRecordValue(args?.arguments); const keyArgs = toolArgs @@ -315,16 +320,34 @@ function buildComposioCallCardData( ? `starting_after: ${readStringValue(output?.starting_after)}` : null, ].filter((value): value is string => Boolean(value)); + const recoveryBits = [ + readStringValue(recovery?.recovered_via), + readStringValue(recovery?.retried_with_account) + ? `account: ${readStringValue(recovery?.retried_with_account)}` + : null, + readStringValue(recovery?.refreshed_execution_ref) + ? "refreshed execution_ref returned" + : null, + ].filter((value): value is string => Boolean(value)); return { - app: readStringValue(args?.app), + app: + readStringValue(args?.app) ?? + readStringValue(output?.toolkit) ?? + readStringValue(execution?.toolkit), toolName, account: readStringValue(args?.account) ?? + readStringValue(output?.account) ?? + readStringValue(execution?.account) ?? readStringValue(args?.connected_account_id) ?? readStringValue(args?.account_identity), keyArgs, pagination: paginationBits.length > 0 ? paginationBits.join(" | ") : null, resultSummary, + sessionId: + readStringValue(output?.tool_router_session_id) ?? + readStringValue(execution?.tool_router_session_id), + recoverySummary: recoveryBits.length > 0 ? recoveryBits.join(" | ") : null, }; } @@ -1366,6 +1389,9 @@ function ToolStep({ : kind === "fetch" ? getFetchDomains(args, output) : []; + const fallbackErrorText = + typeof output?.error === "string" ? output.error : undefined; + const resolvedErrorText = errorText ?? fallbackErrorText; const outputText = typeof output?.text === "string" ? output.text : undefined; @@ -1584,7 +1610,7 @@ function ToolStep({ )} - {status === "error" && errorText && ( + {status === "error" && resolvedErrorText && (
- {errorText} + {resolvedErrorText}
)} - {composioSearchCard && status !== "error" && ( + {composioSearchCard && (
)} - {composioCallCard && status !== "error" && ( + {composioCallCard && (
{composioCallCard.account && ( )} + {composioCallCard.sessionId && ( + + )} + {composioCallCard.recoverySummary && ( + + )} {composioCallCard.keyArgs.length > 0 && ( 0 && (
+				
+					
+					
+					
+					
+				
 			);
 		case "search":
 			return (
diff --git a/apps/web/app/components/chat-message.test.tsx b/apps/web/app/components/chat-message.test.tsx
index cfd54ee83a88..69a41b1d25b6 100644
--- a/apps/web/app/components/chat-message.test.tsx
+++ b/apps/web/app/components/chat-message.test.tsx
@@ -191,4 +191,82 @@ describe("ChatMessage", () => {
 
     expect(button.querySelector('img[src="/integrations/stripe-logomark.svg"]')).toBeNull();
   });
+
+  it("renders persisted Dench Integration failures with their error details", async () => {
+    const user = userEvent.setup();
+
+    render(
+      ,
+    );
+
+    await user.click(screen.getByRole("button", { name: /thought/i }));
+
+    expect(
+      screen.getByText(/Validation failed for tool "composio_call_tool"/),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(/posthog \/ POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS/),
+    ).toBeInTheDocument();
+    expect(screen.getByText(/"project_id": "proj_123"/)).toBeInTheDocument();
+  });
+
+  it("renders live Dench Integration failures with streamed output errors", async () => {
+    const user = userEvent.setup();
+
+    render(
+      ,
+    );
+
+    await user.click(screen.getByRole("button", { name: /thought/i }));
+
+    expect(
+      screen.getByText(/Gateway rejected the bridge invocation./),
+    ).toBeInTheDocument();
+    expect(
+      screen.getByText(/posthog \/ POSTHOG_LIST_ALL_PROJECTS_ACROSS_ORGANIZATIONS/),
+    ).toBeInTheDocument();
+  });
 });
diff --git a/apps/web/app/components/chat-message.tsx b/apps/web/app/components/chat-message.tsx
index beb76e097d8c..24dbfc367bab 100644
--- a/apps/web/app/components/chat-message.tsx
+++ b/apps/web/app/components/chat-message.tsx
@@ -161,13 +161,19 @@ function groupParts(parts: UIMessage["parts"]): MessageSegment[] {
 				status: toolStatus(tp.state, tp.preliminary === true),
 			});
 		} else {
+			const output = asRecord(tp.output);
+			const status = toolStatus(tp.state, tp.preliminary === true);
 			chain.push({
 				kind: "tool",
 				toolName: tp.toolName,
 				toolCallId: tp.toolCallId,
-				status: toolStatus(tp.state, tp.preliminary === true),
+				status,
 				args: asRecord(tp.input),
-				output: asRecord(tp.output),
+				output,
+				errorText:
+					status === "error" && typeof output?.error === "string"
+						? output.error
+						: undefined,
 			});
 		}
 	} else if (part.type.startsWith("tool-")) {
@@ -218,6 +224,7 @@ function groupParts(parts: UIMessage["parts"]): MessageSegment[] {
 				status: toolStatus(resolvedState, tp.preliminary === true),
 				args: asRecord(tp.input) ?? asRecord(tp.args),
 				output: asRecord(tp.output) ?? asRecord(tp.result),
+				errorText: tp.errorText,
 			});
 		}
 	}
@@ -764,18 +771,24 @@ function ComposioActionButton({
 	return (