Feat: Diagnose error action component (#325)

* init diagnose-error component

* rm old diagnose error genui v1 component

* diagnose-error schema to shared/tools

* CR

* Update apps/web/src/components/thread/messages/ai.tsx

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Dylan Boudro 2025-07-02 10:20:45 -07:00 • committed by GitHub
parent 4e52be54f8
commit 942bc5ca1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 132 additions and 136 deletions

View file

@ -9,6 +9,7 @@ import {
GraphUpdate,
PlanItem,
} from "@open-swe/shared/open-swe/types";
import { createDiagnoseErrorToolFields } from "@open-swe/shared/open-swe/tools";
import { formatPlanPromptWithSummaries } from "../../../utils/plan-prompt.js";
import { getMessageString } from "../../../utils/message/content.js";
import { getMessageContentString } from "@open-swe/shared/messages";
@ -56,15 +57,7 @@ const userPrompt = `Here is the full conversation history from the steps taken t
Please carefully go over all of this information, and provide a helpful explanation of exactly what the issue is, and how you can fix it. When you are ready to provide your diagnosis, call the \`diagnose_error\` tool.`;
const diagnoseErrorToolSchema = z.object({
diagnosis: z.string().describe("The diagnosis of the error."),
});
const diagnoseErrorTool = {
name: "diagnose_error",
description: "Diagnoses an error given a diagnosis.",
schema: diagnoseErrorToolSchema,
};
const diagnoseErrorTool = createDiagnoseErrorToolFields();
const formatSystemPrompt = (
lastFailedActionContent: string,
@ -138,7 +131,7 @@ export async function diagnoseError(
}
logger.info("Diagnosed error successfully.", {
diagnosis: (toolCall.args as z.infer<typeof diagnoseErrorToolSchema>)
diagnosis: (toolCall.args as z.infer<typeof diagnoseErrorTool.schema>)
.diagnosis,
});

View file

@ -1,122 +0,0 @@
"use client";
import "../app/globals.css";
import {
AlertTriangle,
Loader2,
CheckCircle,
MessageSquare,
FileText,
} from "lucide-react";
import { useState } from "react";
type DiagnoseErrorProps = {
status: "loading" | "generating" | "done";
diagnosis?: string;
recommendation?: string;
reasoningText?: string;
summaryText?: string;
};
export function DiagnoseError({
status,
diagnosis,
recommendation,
reasoningText,
summaryText,
}: DiagnoseErrorProps) {
const [showReasoning, setShowReasoning] = useState(false);
const [showSummary, setShowSummary] = useState(false);
const getStatusIcon = () => {
switch (status) {
case "loading":
return (
<div className="h-3.5 w-3.5 rounded-full border border-gray-300" />
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
case "done":
return <CheckCircle className="h-3.5 w-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing error analysis...";
case "generating":
return "Diagnosing errors...";
case "done":
return "Error diagnosis complete";
}
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800">
{reasoningText}
</p>
)}
</div>
)}
<div className="flex items-center border-b border-gray-200 bg-gray-50 p-2">
<AlertTriangle className="mr-2 h-3.5 w-3.5 text-amber-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{status === "done" && diagnosis && (
<div className="p-2">
<div className="mb-2">
<h3 className="mb-1 text-xs font-normal text-gray-500">
Diagnosis
</h3>
<p className="text-xs font-normal text-gray-800">{diagnosis}</p>
</div>
{recommendation && (
<div>
<h3 className="mb-1 text-xs font-normal text-gray-500">
Recommendation
</h3>
<p className="text-xs font-normal text-gray-800">
{recommendation}
</p>
</div>
)}
</div>
)}
{summaryText && status === "done" && (
<div className="border-t border-green-100 bg-green-50 p-2">
<button
onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-700 hover:text-green-800"
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}
</button>
{showSummary && (
<p className="mt-1 text-xs font-normal text-green-800">
{summaryText}
</p>
)}
</div>
)}
</div>
);
}

View file

@ -1,5 +1,4 @@
import { ActionStep } from "./action-step";
import { DiagnoseError } from "./diagnose-error";
import { InitializeStep } from "./initialize-step";
import { PushChanges } from "./push-changes";
import { ReplanningStep } from "./replanning-step";
@ -7,7 +6,6 @@ import { TaskSummary } from "./task-summary";
export default {
"action-step": ActionStep,
"diagnose-error": DiagnoseError,
"initialize-step": InitializeStep,
"push-changes": PushChanges,
"replanning-step": ReplanningStep,

View file

@ -21,6 +21,7 @@ import { Interrupt } from "./interrupt";
import { ActionStep, ActionItemProps } from "@/components/gen-ui/action-step";
import { TaskSummary } from "@/components/gen-ui/task-summary";
import { PullRequestOpened } from "@/components/gen-ui/pull-request-opened";
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
import { ToolCall } from "@langchain/core/messages/tool";
import {
createApplyPatchToolFields,
@ -30,6 +31,7 @@ import {
createOpenPrToolFields,
createInstallDependenciesToolFields,
createTakePlannerNotesFields,
createDiagnoseErrorToolFields,
} from "@open-swe/shared/open-swe/tools";
import { z } from "zod";
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
@ -54,6 +56,9 @@ type InstallDependenciesToolArgs = z.infer<
const plannerNotesTool = createTakePlannerNotesFields();
type PlannerNotesToolArgs = z.infer<typeof plannerNotesTool.schema>;
const diagnoseErrorTool = createDiagnoseErrorToolFields();
type DiagnoseErrorToolArgs = z.infer<typeof diagnoseErrorTool.schema>;
function CustomComponent({
message,
thread,
@ -256,6 +261,10 @@ export function AssistantMessage({
? aiToolCalls.find((tc) => tc.name === openPrTool.name)
: undefined;
const diagnoseErrorToolCall = message
? aiToolCalls.find((tc) => tc.name === diagnoseErrorTool.name)
: undefined;
// We can be sure that if the task status tool call is present, it will be the
// only tool call/result we need to render for this message.
if (taskStatusToolCall) {
@ -278,7 +287,25 @@ export function AssistantMessage({
);
}
// Same for PR tool. If this is present, it's the only tool call we need to render.
if (diagnoseErrorToolCall) {
const correspondingToolResult = toolResults.find(
(tr) => tr && tr.tool_call_id === diagnoseErrorToolCall.id,
);
const args = diagnoseErrorToolCall.args as DiagnoseErrorToolArgs;
const reasoningText = getContentString(content);
return (
<div className="flex flex-col gap-4">
<DiagnoseErrorAction
status={correspondingToolResult ? "done" : "generating"}
diagnosis={args.diagnosis}
reasoningText={reasoningText}
/>
</div>
);
}
if (openPrToolCall) {
let branch: string | undefined;
let targetBranch: string | undefined = "main";

View file

@ -0,0 +1,89 @@
"use client";
import { useState } from "react";
import {
AlertTriangle,
Loader2,
CheckCircle,
MessageSquare,
} from "lucide-react";
type DiagnoseErrorActionProps = {
status: "loading" | "generating" | "done";
diagnosis?: string;
reasoningText?: string;
};
export function DiagnoseErrorAction({
status,
diagnosis,
reasoningText,
}: DiagnoseErrorActionProps) {
const [showReasoning, setShowReasoning] = useState(false);
const getStatusIcon = () => {
switch (status) {
case "loading":
return <div className="border-border size-3.5 rounded-full border" />;
case "generating":
return (
<Loader2 className="text-muted-foreground size-3.5 animate-spin" />
);
case "done":
return <CheckCircle className="size-3.5 text-green-500" />;
}
};
const getStatusText = () => {
switch (status) {
case "loading":
return "Preparing error analysis...";
case "generating":
return "Diagnosing errors...";
case "done":
return "Error diagnosis complete";
}
};
return (
<div className="border-border overflow-hidden rounded-md border">
{reasoningText && (
<div className="border-b border-blue-100 bg-blue-50 p-2 dark:border-blue-800 dark:bg-blue-950">
<button
onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-700 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"}
</button>
{showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-800 dark:text-blue-300">
{reasoningText}
</p>
)}
</div>
)}
<div className="border-border flex items-center border-b bg-gray-50 p-2 dark:bg-gray-800">
<AlertTriangle className="mr-2 size-3.5 text-amber-500" />
<span className="text-foreground/80 flex-1 text-xs font-normal">
{getStatusText()}
</span>
{getStatusIcon()}
</div>
{status === "done" && diagnosis && (
<div className="p-2">
<div className="mb-2">
<h3 className="text-muted-foreground mb-1 text-xs font-normal">
Diagnosis
</h3>
<p className="text-foreground/80 text-xs font-normal">
{diagnosis}
</p>
</div>
</div>
)}
</div>
);
}

View file

@ -127,7 +127,6 @@ export function ThreadView({
setChatInput={setChatInput}
handleSendMessage={handleSendMessage}
/>
{/* Right Side - Actions & Plan */}
<div className="flex h-full flex-1 flex-col">
<div className="relative flex-1">

View file

@ -269,3 +269,15 @@ export function createTakePlannerNotesFields() {
"Do not duplicate any information present in the user provided 'custom rules', as we want to avoid duplicating context.",
};
}
export function createDiagnoseErrorToolFields() {
const diagnoseErrorToolSchema = z.object({
diagnosis: z.string().describe("The diagnosis of the error."),
});
return {
name: "diagnose_error",
description: "Diagnoses an error given a diagnosis.",
schema: diagnoseErrorToolSchema,
};
}