From 4a2fe301535b2d178e68dc775d7e07f57f6fdd40 Mon Sep 17 00:00:00 2001
From: Ramnique Singh <30795890+ramnique@users.noreply.github.com>
Date: Mon, 6 Jul 2026 10:56:23 +0530
Subject: [PATCH 1/2] fix(x): surface coding tool errors
---
.../src/components/ai-elements/tool.tsx | 4 +-
.../renderer/src/components/chat-sidebar.tsx | 3 +-
.../src/components/code/code-chat.tsx | 4 +-
.../renderer/src/components/coding-run.tsx | 35 ++++++++++--
.../src/components/compact-conversation.tsx | 3 +-
.../renderer/src/lib/chat-conversation.ts | 13 +++++
.../src/lib/session-chat/turn-view.test.ts | 29 ++++++++++
.../src/lib/session-chat/turn-view.ts | 2 +-
.../skills/code-with-agents/skill.ts | 2 +-
.../src/application/lib/builtin-tools.test.ts | 54 +++++++++++++++++++
.../core/src/application/lib/builtin-tools.ts | 7 +--
11 files changed, 138 insertions(+), 18 deletions(-)
create mode 100644 apps/x/packages/core/src/application/lib/builtin-tools.test.ts
diff --git a/apps/x/apps/renderer/src/components/ai-elements/tool.tsx b/apps/x/apps/renderer/src/components/ai-elements/tool.tsx
index 9635b244..00ae939e 100644
--- a/apps/x/apps/renderer/src/components/ai-elements/tool.tsx
+++ b/apps/x/apps/renderer/src/components/ai-elements/tool.tsx
@@ -22,7 +22,7 @@ import {
import { type ComponentProps, type ReactNode, isValidElement, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import type { ToolCall, ToolGroup as ToolGroupType } from "@/lib/chat-conversation";
-import { getToolActionsSummary, getToolDisplayName, getToolGroupSummary, toToolState } from "@/lib/chat-conversation";
+import { getToolActionsSummary, getToolDisplayName, getToolErrorText, getToolGroupSummary, toToolState } from "@/lib/chat-conversation";
const formatToolValue = (value: unknown) => {
if (typeof value === "string") return value;
@@ -329,7 +329,7 @@ export const ToolGroupComponent = ({ group, isToolOpen, onToolOpenChange }: Tool
diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
index 453b1e64..ce1bc9cf 100644
--- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx
+++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx
@@ -51,6 +51,7 @@ import {
getWebSearchCardData,
getComposioConnectCardData,
getToolDisplayName,
+ getToolErrorText,
groupConversationItems,
isChatMessage,
isErrorMessage,
@@ -484,7 +485,7 @@ export function ChatSidebar({
)
}
const toolTitle = getToolDisplayName(item)
- const errorText = item.status === 'error' ? 'Tool error' : ''
+ const errorText = getToolErrorText(item)
const output = normalizeToolOutput(item.result, item.status)
const input = normalizeToolInput(item.input)
return (
diff --git a/apps/x/apps/renderer/src/components/code/code-chat.tsx b/apps/x/apps/renderer/src/components/code/code-chat.tsx
index 28a55db5..528eceb9 100644
--- a/apps/x/apps/renderer/src/components/code/code-chat.tsx
+++ b/apps/x/apps/renderer/src/components/code/code-chat.tsx
@@ -10,7 +10,7 @@ import { Conversation, ConversationContent, ConversationScrollButton } from '@/c
import { MessageResponse } from '@/components/ai-elements/message'
import { Shimmer } from '@/components/ai-elements/shimmer'
import { Tool, ToolContent, ToolHeader } from '@/components/ai-elements/tool'
-import { toToolState, getToolDisplayName, getWebSearchCardData, type ToolCall } from '@/lib/chat-conversation'
+import { toToolState, getToolDisplayName, getToolErrorText, getWebSearchCardData, type ToolCall } from '@/lib/chat-conversation'
import { CodeRunPermissionRequest, CodingRunTimeline } from '@/components/coding-run'
import { PermissionRequest } from '@/components/ai-elements/permission-request'
import { AskHumanRequest } from '@/components/ai-elements/ask-human-request'
@@ -84,7 +84,7 @@ function RowboatToolCall({ item, onOpenDiff }: { item: ToolCall; onOpenDiff: (pa
-
+
)
diff --git a/apps/x/apps/renderer/src/components/coding-run.tsx b/apps/x/apps/renderer/src/components/coding-run.tsx
index 916aad37..2625b65a 100644
--- a/apps/x/apps/renderer/src/components/coding-run.tsx
+++ b/apps/x/apps/renderer/src/components/coding-run.tsx
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'
import {
+ AlertCircle,
CheckCircle2,
Circle,
CircleDot,
@@ -17,7 +18,7 @@ import {
import type { CodeRunEvent, PermissionAsk, PermissionDecision } from '@x/shared/src/code-mode.js'
import { cn } from '@/lib/utils'
import { Tool, ToolContent, ToolHeader } from '@/components/ai-elements/tool'
-import { toToolState, type ToolCall } from '@/lib/chat-conversation'
+import { getToolErrorText, toToolState, type ToolCall } from '@/lib/chat-conversation'
// ── Timeline reduction ──────────────────────────────────────────────
// The raw ACP stream is a flat list of events; collapse it into ordered rows,
@@ -111,14 +112,26 @@ const basename = (p: string) => p.split(/[\\/]/).pop() || p
export function CodingRunTimeline({
events,
+ error,
onOpenDiff,
}: {
events: CodeRunEvent[]
+ error?: string
// When set, changed-file names become clickable (the Code section opens the diff).
onOpenDiff?: (path: string) => void
}) {
const rows = useMemo(() => reduceEvents(events), [events])
if (rows.length === 0) {
+ if (error) {
+ return (
+
+ )
+ }
return Starting the agent…
}
return (
@@ -133,12 +146,17 @@ export function CodingRunTimeline({
}
if (row.kind === 'tool') {
const running = row.status !== 'completed' && row.status !== 'failed'
+ const failed = row.status === 'failed'
return (
- {running
- ?
- :
}
+ {running ? (
+
+ ) : failed ? (
+
+ ) : (
+
+ )}
{toolKindIcon(row.toolKind, row.title)}
{row.title ?? row.toolKind ?? 'Tool call'}
@@ -189,6 +207,12 @@ export function CodingRunTimeline({
)
})}
+ {error && (
+
+ )}
)
}
@@ -258,12 +282,13 @@ export function CodingRunBlock({
(item.result as { agent?: string } | undefined)?.agent ??
(item.input as { agent?: string } | undefined)?.agent
const title = AGENT_LABEL[agent ?? ''] ?? 'Coding agent'
+ const error = getToolErrorText(item)
return (
<>
-
+
{item.pendingCodePermission && (
diff --git a/apps/x/apps/renderer/src/components/compact-conversation.tsx b/apps/x/apps/renderer/src/components/compact-conversation.tsx
index f1367d5f..e14b7457 100644
--- a/apps/x/apps/renderer/src/components/compact-conversation.tsx
+++ b/apps/x/apps/renderer/src/components/compact-conversation.tsx
@@ -7,6 +7,7 @@ import {
isErrorMessage,
isToolCall,
getToolDisplayName,
+ getToolErrorText,
toToolState,
normalizeToolOutput,
} from '@/lib/chat-conversation'
@@ -62,7 +63,7 @@ function CompactToolRow({ tool }: { tool: ToolCall }) {
const [open, setOpen] = useState(false)
const title = getToolDisplayName(tool)
const state = toToolState(tool.status)
- const errorText = tool.status === 'error' && typeof tool.result === 'string' ? tool.result : undefined
+ const errorText = getToolErrorText(tool)
return (
diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts
index f9a00c74..0911ac28 100644
--- a/apps/x/apps/renderer/src/lib/chat-conversation.ts
+++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts
@@ -121,6 +121,19 @@ export const normalizeToolOutput = (
return output
}
+export const getToolErrorText = (tool: ToolCall): string | undefined => {
+ if (tool.status !== 'error') return undefined
+ if (typeof tool.result === 'string' && tool.result.trim()) return tool.result
+ if (tool.result !== undefined) {
+ try {
+ return JSON.stringify(tool.result, null, 2)
+ } catch {
+ // Fall through to the generic label for non-serializable legacy values.
+ }
+ }
+ return 'Tool error'
+}
+
export type WebSearchCardResult = { title: string; url: string; description: string }
export type WebSearchCardData = {
diff --git a/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts b/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts
index 342dc16f..34df50ba 100644
--- a/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts
+++ b/apps/x/apps/renderer/src/lib/session-chat/turn-view.test.ts
@@ -190,6 +190,35 @@ describe('buildTurnConversation', () => {
expect(items.map((i) => i.status)).toEqual(['pending', 'running'])
})
+ it('uses the tool result envelope to determine error status', () => {
+ const state = reduceTurn([
+ created(T1, S1),
+ requested(T1, 0),
+ completed(
+ T1,
+ 0,
+ assistantCalls(
+ toolCallPart('flagged', 'echo'),
+ toolCallPart('normal', 'echo'),
+ ),
+ ),
+ invocation(T1, 'flagged', 'echo'),
+ {
+ type: 'tool_result',
+ turnId: T1,
+ ts: TS,
+ toolCallId: 'flagged',
+ toolName: 'echo',
+ source: 'sync',
+ result: { output: 'boom', isError: true },
+ },
+ invocation(T1, 'normal', 'echo'),
+ toolResult(T1, 'normal', 'echo', { success: false, message: 'payload data' }),
+ ])
+ const items = buildTurnConversation(state).filter(isToolCall)
+ expect(items.map((i) => i.status)).toEqual(['error', 'completed'])
+ })
+
it('renders user attachments and a failed turn as an error item', () => {
const input = {
role: 'user' as const,
diff --git a/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts b/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts
index 0d5413c1..2e8df218 100644
--- a/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts
+++ b/apps/x/apps/renderer/src/lib/session-chat/turn-view.ts
@@ -200,7 +200,7 @@ function extractAttachments(content: UserContent): MessageAttachment[] | undefin
}
function toolStatus(tc: ToolCallState): ToolCall['status'] {
- if (tc.result) return 'completed'
+ if (tc.result) return tc.result.result.isError ? 'error' : 'completed'
if (tc.permission && !tc.permission.resolved) return 'pending'
return 'running'
}
diff --git a/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
index d9f15dd8..9feeed36 100644
--- a/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
+++ b/apps/x/packages/core/src/application/assistant/skills/code-with-agents/skill.ts
@@ -79,7 +79,7 @@ After \`code_agent_run\` returns:
- Pass through the agent's \`summary\` as-is. Do not rewrite it.
- Refer to file paths as plain text. Do NOT use \`\`\`file:path\`\`\` reference blocks. (This overrides the global "always wrap paths in filepath blocks" rule — for code-mode output, plain text.)
- Only add your own explanation if it failed:
- - \`success: false\` with a message — surface the message. If it mentions the agent isn't installed or signed in, tell the user to install or sign in via **Settings → Code Mode**.
+ - A tool error with a message — surface the message. If it mentions the agent isn't installed or signed in, tell the user to install or sign in via **Settings → Code Mode**.
- \`stopReason: "cancelled"\` — the run was stopped; acknowledge briefly and ask if they want to continue.
---
diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.test.ts b/apps/x/packages/core/src/application/lib/builtin-tools.test.ts
new file mode 100644
index 00000000..1842024c
--- /dev/null
+++ b/apps/x/packages/core/src/application/lib/builtin-tools.test.ts
@@ -0,0 +1,54 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import container from "../../di/container.js";
+import { InMemoryAbortRegistry } from "../../runs/abort-registry.js";
+import { BuiltinTools } from "./builtin-tools.js";
+import type { ToolContext } from "./exec-tool.js";
+
+function context(signal: AbortSignal): ToolContext {
+ return {
+ runId: "turn-1",
+ toolCallId: "tool-1",
+ signal,
+ abortRegistry: new InMemoryAbortRegistry(),
+ publish: async () => {},
+ codePolicy: "ask",
+ };
+}
+
+function mockCodeServices(runPrompt: () => Promise): void {
+ vi.spyOn(container, "resolve").mockImplementation(((name: string) => {
+ if (name === "codeModeManager") return { runPrompt };
+ if (name === "codePermissionRegistry") {
+ return { cancelRun: vi.fn(), request: vi.fn() };
+ }
+ throw new Error(`Unexpected dependency: ${name}`);
+ }) as typeof container.resolve);
+}
+
+describe("code_agent_run", () => {
+ afterEach(() => vi.restoreAllMocks());
+
+ it("throws genuine coding-agent failures for the runtime to mark as errors", async () => {
+ mockCodeServices(async () => {
+ throw new Error("spawn Electron ENOENT");
+ });
+
+ await expect(BuiltinTools.code_agent_run.execute(
+ { agent: "codex", cwd: "/repo", prompt: "Fix it" },
+ context(new AbortController().signal),
+ )).rejects.toThrow("Coding agent failed: spawn Electron ENOENT");
+ });
+
+ it("returns an ordinary cancellation result when the turn was aborted", async () => {
+ const controller = new AbortController();
+ controller.abort();
+ mockCodeServices(async () => {
+ throw new Error("cancelled");
+ });
+
+ await expect(BuiltinTools.code_agent_run.execute(
+ { agent: "codex", cwd: "/repo", prompt: "Fix it" },
+ context(controller.signal),
+ )).resolves.toMatchObject({ success: false, stopReason: "cancelled" });
+ });
+});
diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts
index f3d16e3f..2f74a9c5 100644
--- a/apps/x/packages/core/src/application/lib/builtin-tools.ts
+++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts
@@ -868,7 +868,7 @@ export const BuiltinTools: z.infer = {
}),
execute: async ({ agent, cwd, prompt }: { agent: 'claude' | 'codex', cwd: string, prompt: string }, ctx?: ToolContext) => {
if (!ctx) {
- return { success: false, message: 'code_agent_run requires run context (runId / streaming).' };
+ throw new Error('code_agent_run requires run context (runId / streaming).');
}
// The composer chip is the source of truth for the agent. The model's `agent`
// argument is only a fallback for the ask-human flow (code mode not active, no
@@ -955,10 +955,7 @@ export const BuiltinTools: z.infer = {
changedFiles: [...changedFiles],
};
}
- return {
- success: false,
- message: `Coding agent failed: ${error instanceof Error ? error.message : String(error)}`,
- };
+ throw new Error(`Coding agent failed: ${error instanceof Error ? error.message : String(error)}`);
} finally {
ctx.signal.removeEventListener('abort', onAbort);
}
From c1b5291f7da60c9510e4d6f86581d6022aaf0c60 Mon Sep 17 00:00:00 2001
From: Ramnique Singh <30795890+ramnique@users.noreply.github.com>
Date: Mon, 6 Jul 2026 10:56:34 +0530
Subject: [PATCH 2/2] approve baileys build
---
apps/x/pnpm-workspace.yaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/x/pnpm-workspace.yaml b/apps/x/pnpm-workspace.yaml
index c588042e..d46f98a2 100644
--- a/apps/x/pnpm-workspace.yaml
+++ b/apps/x/pnpm-workspace.yaml
@@ -16,6 +16,7 @@ catalog:
vitest: 4.1.7
onlyBuiltDependencies:
+ - baileys
- core-js
- electron
- electron-winstaller
@@ -23,6 +24,7 @@ onlyBuiltDependencies:
- fs-xattr
- macos-alias
- protobufjs
+
patchedDependencies:
'@agentclientprotocol/codex-acp@0.0.44': patches/@agentclientprotocol__codex-acp@0.0.44.patch
'@openai/codex@0.128.0': patches/@openai__codex@0.128.0.patch