{props.url}
@@ -371,28 +345,44 @@ function ActionItem(props: ActionItemProps) {
);
}
- if (props.actionType === "find_instances_of") {
+ if (props.actionType === "search") {
+ const castProps = props as SearchActionProps;
return (
-
- {props.query}
-
-
-
+ {castProps.pattern}
+
+
+
- {formattedRgCommand}
-
-
+
{props.file_path}
);
@@ -467,8 +435,7 @@ function ActionItem(props: ActionItemProps) {
if (
(props.actionType === "shell" ||
- props.actionType === "rg" ||
- props.actionType === "find_instances_of" ||
+ props.actionType === "search" ||
props.actionType === "install_dependencies") &&
props.output
) {
@@ -547,10 +514,10 @@ function ActionItem(props: ActionItemProps) {
return (
-
+
{renderHeaderIcon()}
{renderHeaderContent()}
-
+
{getStatusText()}
@@ -558,7 +525,7 @@ function ActionItem(props: ActionItemProps) {
{shouldShowToggle() && (
{showSummary && (
-
- {summaryText}
-
+
+ {summaryText}
+
)}
)}
diff --git a/apps/web/src/components/gen-ui/pull-request-opened.tsx b/apps/web/src/components/gen-ui/pull-request-opened.tsx
index 5dc3167c..971a4e8e 100644
--- a/apps/web/src/components/gen-ui/pull-request-opened.tsx
+++ b/apps/web/src/components/gen-ui/pull-request-opened.tsx
@@ -35,10 +35,12 @@ export function PullRequestOpened({
switch (status) {
case "loading":
return (
-
+
);
case "generating":
- return ;
+ return (
+
+ );
case "done":
return ;
}
@@ -62,28 +64,28 @@ export function PullRequestOpened({
};
return (
-
-
-
+
+
+
{title && status === "done" && (
-
+
{title}
)}
{branch && status === "done" && (
-
+
{branch} → {targetBranch}
)}
{!title && (
-
+
{getStatusText()}
)}
-
+
{getStatusText()}
{getStatusIcon()}
@@ -92,7 +94,7 @@ export function PullRequestOpened({
href={url}
target="_blank"
rel="noopener noreferrer"
- className="text-gray-500 hover:text-gray-700"
+ className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
title="Open pull request"
>
@@ -101,7 +103,7 @@ export function PullRequestOpened({
{shouldShowToggle() && (
{expanded && description && status === "done" && (
-
-
+
+
Description
-
+
{description}
diff --git a/apps/web/src/components/gen-ui/push-changes.tsx b/apps/web/src/components/gen-ui/push-changes.tsx
index daf613e0..165a496c 100644
--- a/apps/web/src/components/gen-ui/push-changes.tsx
+++ b/apps/web/src/components/gen-ui/push-changes.tsx
@@ -12,6 +12,7 @@ import {
MessageSquare,
FileText,
} from "lucide-react";
+import { BasicMarkdownText } from "../thread/markdown-text";
type PushChangesProps = {
status: "loading" | "generating" | "done";
@@ -35,8 +36,8 @@ export function PushChanges({
const [expanded, setExpanded] = useState(
Boolean(status === "done" && gitStatus),
);
- const [showReasoning, setShowReasoning] = useState(false);
- const [showSummary, setShowSummary] = useState(false);
+ const [showReasoning, setShowReasoning] = useState(true);
+ const [showSummary, setShowSummary] = useState(true);
const getStatusIcon = () => {
switch (status) {
@@ -152,9 +153,9 @@ export function PushChanges({
{showSummary ? "Hide summary" : "Show summary"}
{showSummary && (
-
+
{summaryText}
-
+
)}
)}
diff --git a/apps/web/src/components/gen-ui/replanning-step.tsx b/apps/web/src/components/gen-ui/replanning-step.tsx
index 70571b63..982ddd69 100644
--- a/apps/web/src/components/gen-ui/replanning-step.tsx
+++ b/apps/web/src/components/gen-ui/replanning-step.tsx
@@ -9,6 +9,7 @@ import {
FileText,
} from "lucide-react";
import { useState } from "react";
+import { BasicMarkdownText } from "../thread/markdown-text";
type ReplanningStepProps = {
status: "loading" | "generating" | "done";
@@ -21,8 +22,8 @@ export function ReplanningStep({
reasoningText,
summaryText,
}: ReplanningStepProps) {
- const [showReasoning, setShowReasoning] = useState(false);
- const [showSummary, setShowSummary] = useState(false);
+ const [showReasoning, setShowReasoning] = useState(true);
+ const [showSummary, setShowSummary] = useState(true);
const getStatusIcon = () => {
switch (status) {
@@ -85,9 +86,9 @@ export function ReplanningStep({
{showSummary ? "Hide summary" : "Show summary"}
{showSummary && (
-
+
{summaryText}
-
+
)}
)}
diff --git a/apps/web/src/components/gen-ui/task-review.tsx b/apps/web/src/components/gen-ui/task-review.tsx
new file mode 100644
index 00000000..2f44a97b
--- /dev/null
+++ b/apps/web/src/components/gen-ui/task-review.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import { useState } from "react";
+import {
+ CheckCircle,
+ XCircle,
+ Loader2,
+ ChevronDown,
+ ChevronUp,
+ MessageSquare,
+ FileText,
+} from "lucide-react";
+import { BasicMarkdownText } from "../thread/markdown-text";
+
+type MarkTaskCompletedProps = {
+ status: "loading" | "generating" | "done";
+ review?: string;
+ reasoningText?: string;
+ summaryText?: string;
+};
+
+export function MarkTaskCompleted({
+ status,
+ review,
+ reasoningText,
+ summaryText,
+}: MarkTaskCompletedProps) {
+ const [expanded, setExpanded] = useState(!!(status === "done" && review));
+ const [showReasoning, setShowReasoning] = useState(true);
+ const [showSummary, setShowSummary] = useState(true);
+
+ const getStatusIcon = () => {
+ switch (status) {
+ case "loading":
+ return (
+
+ );
+ case "generating":
+ return (
+
+ );
+ case "done":
+ return (
+
+ );
+ }
+ };
+
+ const getStatusText = () => {
+ switch (status) {
+ case "loading":
+ return "Preparing task review...";
+ case "generating":
+ return "Reviewing task completion...";
+ case "done":
+ return "Task marked as completed";
+ }
+ };
+
+ return (
+
+ {reasoningText && (
+
+
+ {showReasoning && (
+
+ {reasoningText}
+
+ )}
+
+ )}
+
+ setExpanded((prev) => !prev)
+ : undefined
+ }
+ >
+
+
+ {getStatusText()}
+
+
+ {getStatusIcon()}
+ {status === "done" && review && (
+
+ )}
+
+
+
+ {expanded && review && status === "done" && (
+
+
+ Final Review
+
+
+ {review}
+
+
+ )}
+
+ {summaryText && status === "done" && (
+
+
+ {showSummary && (
+
+ {summaryText}
+
+ )}
+
+ )}
+
+ );
+}
+
+type MarkTaskIncompleteProps = {
+ status: "loading" | "generating" | "done";
+ review?: string;
+ additionalActions?: string[];
+ reasoningText?: string;
+ summaryText?: string;
+};
+
+export function MarkTaskIncomplete({
+ status,
+ review,
+ additionalActions,
+ reasoningText,
+ summaryText,
+}: MarkTaskIncompleteProps) {
+ const [expanded, setExpanded] = useState(
+ !!(status === "done" && (review || additionalActions)),
+ );
+ const [showReasoning, setShowReasoning] = useState(true);
+ const [showSummary, setShowSummary] = useState(true);
+
+ const getStatusIcon = () => {
+ switch (status) {
+ case "loading":
+ return (
+
+ );
+ case "generating":
+ return ;
+ case "done":
+ return ;
+ }
+ };
+
+ const getStatusText = () => {
+ switch (status) {
+ case "loading":
+ return "Preparing task review...";
+ case "generating":
+ return "Reviewing task completion...";
+ case "done":
+ return "Task marked as incomplete";
+ }
+ };
+
+ return (
+
+ {reasoningText && (
+
+
+ {showReasoning && (
+
+ {reasoningText}
+
+ )}
+
+ )}
+
+ setExpanded((prev) => !prev)
+ : undefined
+ }
+ >
+
+
+ {getStatusText()}
+
+
+ {getStatusIcon()}
+ {status === "done" && (review || additionalActions) && (
+
+ )}
+
+
+
+ {expanded && status === "done" && (review || additionalActions) && (
+
+ {review && (
+
+
+ Final Review
+
+
+ {review}
+
+
+ )}
+
+ {additionalActions && additionalActions.length > 0 && (
+
+
+ Additional Actions Required ({additionalActions.length})
+
+
+ {additionalActions.map((action, index) => (
+ -
+
+
+ {index + 1}
+
+
+
+ {action}
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ {summaryText && status === "done" && (
+
+
+ {showSummary && (
+
+ {summaryText}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/gen-ui/task-summary.tsx b/apps/web/src/components/gen-ui/task-summary.tsx
index 087513cb..2aa02857 100644
--- a/apps/web/src/components/gen-ui/task-summary.tsx
+++ b/apps/web/src/components/gen-ui/task-summary.tsx
@@ -9,22 +9,22 @@ import {
FileText,
MinusCircle,
} from "lucide-react";
+import { cn } from "@/lib/utils";
+import { BasicMarkdownText } from "../thread/markdown-text";
type TaskSummaryProps = {
status: "loading" | "generating" | "done";
completed?: boolean;
- summary?: string;
summaryText?: string;
};
export function TaskSummary({
status,
completed,
- summary,
summaryText,
}: TaskSummaryProps) {
const [expanded, setExpanded] = useState(false);
- const [showSummary, setShowSummary] = useState(false);
+ const [showSummary, setShowSummary] = useState(true);
const getStatusIcon = () => {
switch (status) {
@@ -59,9 +59,9 @@ export function TaskSummary({
return (
setExpanded(!expanded)
: undefined
}
@@ -70,26 +70,8 @@ export function TaskSummary({
{getStatusText()}
- {status === "done" && summary && (
-
- )}
- {expanded && summary && status === "done" && (
-
-
- Task Summary
-
- {summary}
-
- )}
-
{summaryText && status === "done" && (
{showSummary && (
-
{summaryText}
-
+
)}
)}
diff --git a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
index 737cf61c..c2fa38e0 100644
--- a/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
+++ b/apps/web/src/components/github/repo-branch-selectors/branch-selector.tsx
@@ -193,7 +193,7 @@ export function BranchSelector({
{selectedBranch || placeholder}
-
+
@@ -230,13 +230,13 @@ export function BranchSelector({
{branch.name}
{isDefault && (
-
+
default
)}
{branch.protected && (
-
+
)}
diff --git a/apps/web/src/components/github/repo-branch-selectors/index.tsx b/apps/web/src/components/github/repo-branch-selectors/index.tsx
index 4091ce49..134bf6a4 100644
--- a/apps/web/src/components/github/repo-branch-selectors/index.tsx
+++ b/apps/web/src/components/github/repo-branch-selectors/index.tsx
@@ -6,12 +6,12 @@ export function RepositoryBranchSelectors() {
const [threadId] = useQueryState("threadId");
const chatStarted = !!threadId;
const defaultButtonStyles =
- "bg-inherit border-none text-gray-500 hover:text-black dark:hover:text-gray-300 text-xs p-0 px-0 py-0 !p-0 !px-0 !py-0 h-fit hover:bg-inherit shadow-none";
+ "bg-inherit border-none text-muted-foreground hover:text-muted-foreground/70 text-xs p-0 px-0 py-0 !p-0 !px-0 !py-0 h-fit hover:bg-inherit shadow-none";
const defaultStylesChatStarted =
- "hover:bg-inherit cursor-default hover:cursor-default hover:text-black dark:hover:text-gray-300 hover:border-gray-300 hover:ring-inherit shadow-none p-0 px-0 py-0 !p-0 !px-0 !py-0";
+ "hover:bg-inherit cursor-default hover:cursor-default text-muted-foreground hover:border-gray-300 hover:ring-inherit shadow-none p-0 px-0 py-0 !p-0 !px-0 !py-0";
return (
-
+
-
+
diff --git a/apps/web/src/components/thread/markdown-text.tsx b/apps/web/src/components/thread/markdown-text.tsx
index a0d51663..be0f7a17 100644
--- a/apps/web/src/components/thread/markdown-text.tsx
+++ b/apps/web/src/components/thread/markdown-text.tsx
@@ -114,7 +114,7 @@ const defaultComponents: any = {
),
p: ({ className, ...props }: { className?: string }) => (
),
@@ -135,13 +135,13 @@ const defaultComponents: any = {
),
ul: ({ className, ...props }: { className?: string }) => (
li]:mt-2", className)}
+ className={cn("my-2 ml-6 list-disc [&>li]:mt-1", className)}
{...props}
/>
),
ol: ({ className, ...props }: { className?: string }) => (
li]:mt-2", className)}
+ className={cn("my-2 ml-6 list-decimal [&>li]:mt-2", className)}
{...props}
/>
),
@@ -243,9 +243,12 @@ const defaultComponents: any = {
},
};
-const MarkdownTextImpl: FC<{ children: string }> = ({ children }) => {
+const MarkdownTextImpl: FC<{ children: string; className?: string }> = ({
+ children,
+ className,
+}) => {
return (
-
+
= ({ children }) => {
};
export const MarkdownText = memo(MarkdownTextImpl);
+
+const BasicMarkdownTextImpl: FC<{ children: string; className?: string }> = ({
+ children,
+ className,
+}) => {
+ const basicMarkdownComponents = { ...defaultComponents };
+ // Don't render headers, instead render them as bold text
+ delete basicMarkdownComponents.h1;
+ delete basicMarkdownComponents.h2;
+ delete basicMarkdownComponents.h3;
+ delete basicMarkdownComponents.h4;
+ delete basicMarkdownComponents.h5;
+ delete basicMarkdownComponents.h6;
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export const BasicMarkdownText = memo(BasicMarkdownTextImpl);
diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx
index dba1dce5..5f769e1d 100644
--- a/apps/web/src/components/thread/messages/ai.tsx
+++ b/apps/web/src/components/thread/messages/ai.tsx
@@ -21,6 +21,10 @@ 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 {
+ MarkTaskCompleted,
+ MarkTaskIncomplete,
+} from "@/components/gen-ui/task-review";
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
import { WriteTechnicalNotes } from "@/components/gen-ui/write-technical-notes";
import { ToolCall } from "@langchain/core/messages/tool";
@@ -29,13 +33,14 @@ import {
createShellToolFields,
createMarkTaskCompletedToolFields,
createMarkTaskNotCompletedToolFields,
- createRgToolFields,
+ createSearchToolFields,
createOpenPrToolFields,
createInstallDependenciesToolFields,
createTakePlannerNotesFields,
+ createCodeReviewMarkTaskCompletedFields,
+ createCodeReviewMarkTaskNotCompleteFields,
createDiagnoseErrorToolFields,
createGetURLContentToolFields,
- createFindInstancesOfToolFields,
createWriteTechnicalNotesToolFields,
createConversationHistorySummaryToolFields,
} from "@open-swe/shared/open-swe/tools";
@@ -56,8 +61,8 @@ const markTaskNotCompletedTool = createMarkTaskNotCompletedToolFields();
type MarkTaskNotCompletedToolArgs = z.infer<
typeof markTaskNotCompletedTool.schema
>;
-const rgTool = createRgToolFields(dummyRepo);
-type RgToolArgs = z.infer;
+const searchTool = createSearchToolFields(dummyRepo);
+type SearchToolArgs = z.infer;
const openPrTool = createOpenPrToolFields();
type OpenPrToolArgs = z.infer;
const installDependenciesTool = createInstallDependenciesToolFields(dummyRepo);
@@ -66,6 +71,16 @@ type InstallDependenciesToolArgs = z.infer<
>;
const plannerNotesTool = createTakePlannerNotesFields();
type PlannerNotesToolArgs = z.infer;
+const markFinalReviewTaskCompletedTool =
+ createCodeReviewMarkTaskCompletedFields();
+type MarkFinalReviewTaskCompletedToolArgs = z.infer<
+ typeof markFinalReviewTaskCompletedTool.schema
+>;
+const markFinalReviewTaskIncompleteTool =
+ createCodeReviewMarkTaskNotCompleteFields();
+type MarkFinalReviewTaskIncompleteToolArgs = z.infer<
+ typeof markFinalReviewTaskIncompleteTool.schema
+>;
const diagnoseErrorTool = createDiagnoseErrorToolFields();
type DiagnoseErrorToolArgs = z.infer;
@@ -73,9 +88,6 @@ type DiagnoseErrorToolArgs = z.infer;
const getURLContentTool = createGetURLContentToolFields();
type GetURLContentToolArgs = z.infer;
-const findInstancesOfTool = createFindInstancesOfToolFields(dummyRepo);
-type FindInstancesOfToolArgs = z.infer;
-
const writeTechnicalNotesTool = createWriteTechnicalNotesToolFields();
type WriteTechnicalNotesToolArgs = z.infer<
typeof writeTechnicalNotesTool.schema
@@ -182,14 +194,21 @@ export function mapToolMessageToActionStepProps(
reasoningText,
errorMessage: !success ? getContentString(message.content) : undefined,
};
- } else if (toolCall?.name === rgTool.name) {
- const args = toolCall.args as RgToolArgs;
+ } else if (toolCall?.name === searchTool.name) {
+ const args = toolCall.args as SearchToolArgs;
return {
- actionType: "rg",
+ actionType: "search",
status,
success,
pattern: args.pattern || "",
- paths: args.paths || [],
+ regex: args.regex || false,
+ case_sensitive: args.case_sensitive || false,
+ context_lines: args.context_lines || 0,
+ max_results: args.max_results || 0,
+ follow_symlinks: args.follow_symlinks || false,
+ exclude_files: args.exclude_files || "",
+ include_files: args.include_files || "",
+ file_types: args.file_types || [],
output: getContentString(message.content),
reasoningText,
};
@@ -223,24 +242,6 @@ export function mapToolMessageToActionStepProps(
output: getContentString(message.content),
reasoningText,
};
- } else if (toolCall?.name === findInstancesOfTool.name) {
- const args = toolCall.args as FindInstancesOfToolArgs;
- // case_sensitive and match_word both default to true.
- const caseSensitive =
- args.case_sensitive === undefined ? true : args.case_sensitive;
- const matchWord = args.match_word === undefined ? true : args.match_word;
- return {
- actionType: "find_instances_of",
- status,
- success,
- query: args.query || "",
- case_sensitive: caseSensitive,
- match_word: matchWord,
- include_files: args.include_files,
- exclude_files: args.exclude_files,
- output: getContentString(message.content),
- reasoningText,
- };
}
return {
status: "loading",
@@ -305,11 +306,10 @@ export function AssistantMessage({
(tc) =>
tc.name === shellTool.name ||
tc.name === applyPatchTool.name ||
- tc.name === rgTool.name ||
+ tc.name === searchTool.name ||
tc.name === installDependenciesTool.name ||
tc.name === plannerNotesTool.name ||
- tc.name === getURLContentTool.name ||
- tc.name === findInstancesOfTool.name,
+ tc.name === getURLContentTool.name,
)
: [];
@@ -325,6 +325,18 @@ export function AssistantMessage({
? aiToolCalls.find((tc) => tc.name === openPrTool.name)
: undefined;
+ const markFinalReviewTaskCompletedToolCall = message
+ ? aiToolCalls.find(
+ (tc) => tc.name === markFinalReviewTaskCompletedTool.name,
+ )
+ : undefined;
+
+ const markFinalReviewTaskIncompleteToolCall = message
+ ? aiToolCalls.find(
+ (tc) => tc.name === markFinalReviewTaskIncompleteTool.name,
+ )
+ : undefined;
+
const diagnoseErrorToolCall = message
? aiToolCalls.find((tc) => tc.name === diagnoseErrorTool.name)
: undefined;
@@ -472,6 +484,50 @@ export function AssistantMessage({
);
}
+ // If task completed review tool call is present, render the task review component
+ if (markFinalReviewTaskCompletedToolCall) {
+ const args =
+ markFinalReviewTaskCompletedToolCall.args as MarkFinalReviewTaskCompletedToolArgs;
+ const correspondingToolResult = toolResults.find(
+ (tr) => tr && tr.tool_call_id === markFinalReviewTaskCompletedToolCall.id,
+ );
+
+ const status = correspondingToolResult ? "done" : "generating";
+
+ return (
+
+
+
+ );
+ }
+
+ // If task incomplete review tool call is present, render the task review component
+ if (markFinalReviewTaskIncompleteToolCall) {
+ const args =
+ markFinalReviewTaskIncompleteToolCall.args as MarkFinalReviewTaskIncompleteToolArgs;
+ const correspondingToolResult = toolResults.find(
+ (tr) =>
+ tr && tr.tool_call_id === markFinalReviewTaskIncompleteToolCall.id,
+ );
+
+ const status = correspondingToolResult ? "done" : "generating";
+
+ return (
+
+
+
+ );
+ }
+
if (actionableToolCalls.length > 0) {
const actionItems = actionableToolCalls.map((toolCall): ActionItemProps => {
const correspondingToolResult = toolResults.find(
@@ -479,7 +535,7 @@ export function AssistantMessage({
);
const isShellTool = toolCall.name === shellTool.name;
- const isRgTool = toolCall.name === rgTool.name;
+ const isSearchTool = toolCall.name === searchTool.name;
const isInstallDependenciesTool =
toolCall.name === installDependenciesTool.name;
@@ -489,13 +545,20 @@ export function AssistantMessage({
correspondingToolResult,
threadMessages,
);
- } else if (isRgTool) {
- const args = toolCall.args as RgToolArgs;
+ } else if (isSearchTool) {
+ const args = toolCall.args as SearchToolArgs;
return {
- actionType: "rg",
+ actionType: "search",
status: "generating",
pattern: args?.pattern || "",
- paths: args?.paths || [],
+ regex: args?.regex || false,
+ case_sensitive: args?.case_sensitive || false,
+ context_lines: args?.context_lines || 0,
+ max_results: args?.max_results || 0,
+ follow_symlinks: args?.follow_symlinks || false,
+ exclude_files: args?.exclude_files || [],
+ include_files: args?.include_files || [],
+ file_types: args?.file_types || [],
output: "",
} as ActionItemProps;
} else if (isInstallDependenciesTool) {
@@ -522,23 +585,6 @@ export function AssistantMessage({
url: args?.url || "",
output: "",
} as ActionItemProps;
- } else if (toolCall.name === findInstancesOfTool.name) {
- const args = toolCall.args as FindInstancesOfToolArgs;
- // case_sensitive and match_word both default to true.
- const caseSensitive =
- args.case_sensitive === undefined ? true : args.case_sensitive;
- const matchWord =
- args.match_word === undefined ? true : args.match_word;
- return {
- actionType: "find_instances_of",
- status: "generating",
- query: args?.query || "",
- case_sensitive: caseSensitive,
- match_word: matchWord,
- include_files: args?.include_files,
- exclude_files: args?.exclude_files,
- output: "",
- } as ActionItemProps;
} else {
if (isShellTool) {
const args = toolCall.args as ShellToolArgs;
diff --git a/apps/web/src/components/v2/actions-renderer.tsx b/apps/web/src/components/v2/actions-renderer.tsx
index 2eec3466..3d1db5c9 100644
--- a/apps/web/src/components/v2/actions-renderer.tsx
+++ b/apps/web/src/components/v2/actions-renderer.tsx
@@ -2,7 +2,10 @@ import { isAIMessageSDK, isHumanMessageSDK } from "@/lib/langchain-messages";
import { UseStream, useStream } from "@langchain/langgraph-sdk/react";
import { AssistantMessage } from "../thread/messages/ai";
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
-import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
+import {
+ ManagerGraphState,
+ ManagerGraphUpdate,
+} from "@open-swe/shared/open-swe/manager/types";
import { useCancelStream } from "@/hooks/useCancelStream";
import {
isCustomNodeEvent,
@@ -102,9 +105,22 @@ function addMessagesToState(
newMessages: Message[],
): Message[] {
const existingIds = new Set(existingMessages.map((message) => message.id));
- const uniqueNewMessages = newMessages.filter(
- (message) => !message.id || !existingIds.has(message.id),
- );
+
+ // First deduplicate within newMessages array itself
+ const seenNewIds = new Set();
+ const uniqueNewMessages = newMessages.filter((message) => {
+ // Skip messages without IDs or those already in existingMessages
+ if (message.id && existingIds.has(message.id)) return false;
+
+ // Handle duplicates within newMessages
+ if (message.id) {
+ if (seenNewIds.has(message.id)) return false;
+ seenNewIds.add(message.id);
+ }
+
+ return true;
+ });
+
return [...existingMessages, ...uniqueNewMessages];
}
@@ -123,9 +139,28 @@ function isNodeEndMessagesUpdate(
);
}
+function isNodeEndCommandUpdate(data: unknown): data is {
+ output: { lg_name: string; goto: string; update: ManagerGraphUpdate };
+} {
+ return !!(
+ typeof data === "object" &&
+ data !== null &&
+ "output" in data &&
+ data.output &&
+ typeof data.output === "object" &&
+ "lg_name" in data.output &&
+ "goto" in data.output &&
+ "update" in data.output &&
+ typeof data.output.lg_name === "string" &&
+ (typeof data.output.goto === "string" || Array.isArray(data.output.goto)) &&
+ typeof data.output.update === "object"
+ );
+}
+
const REVIEWER_NODE_IDS = [
- "take-review-actions",
"generate-review-actions",
+ "take-review-actions",
+ "diagnose-reviewer-error",
"final-review",
];
@@ -144,6 +179,11 @@ export function ActionsRenderer({
const joinedRunId = useRef(undefined);
const [streamLoading, setStreamLoading] = useState(false);
const [mergedMessages, setMergedMessages] = useState([]);
+ const debouncedSetMessages = useRef(
+ debounce((messages: Message[]) => {
+ setMergedMessages((prev) => addMessagesToState(prev, messages));
+ }, 100),
+ ).current;
const stream = useStream({
apiUrl: process.env.NEXT_PUBLIC_API_URL,
@@ -160,11 +200,18 @@ export function ActionsRenderer({
data.event === "on_chain_end" &&
data.metadata?.langgraph_node &&
REVIEWER_NODE_IDS.includes(data.metadata.langgraph_node as string) &&
- data.data &&
- isNodeEndMessagesUpdate(data.data)
+ data.data
) {
- const outputMessages = data.data.output.messages;
- setMergedMessages((prev) => [...prev, ...outputMessages]);
+ if (isNodeEndCommandUpdate(data.data)) {
+ const outputMessages = data.data.output.update
+ .messages as unknown as Message[];
+ console.log("outputMessages", outputMessages);
+ debouncedSetMessages(outputMessages);
+ } else if (isNodeEndMessagesUpdate(data.data)) {
+ const outputMessages = data.data.output.messages;
+ console.log("outputMessages", outputMessages);
+ debouncedSetMessages(outputMessages);
+ }
}
},
fetchStateHistory: false,
@@ -290,12 +337,6 @@ export function ActionsRenderer({
}
}, [stream.values, graphId]);
- const debouncedSetMessages = useRef(
- debounce((messages: Message[]) => {
- setMergedMessages((prev) => addMessagesToState(prev, messages));
- }, 100),
- ).current;
-
useEffect(() => {
debouncedSetMessages(stream.messages);
return () => {
diff --git a/apps/web/src/components/v2/diagnose-error-action.tsx b/apps/web/src/components/v2/diagnose-error-action.tsx
index 0319d4fe..dd43461f 100644
--- a/apps/web/src/components/v2/diagnose-error-action.tsx
+++ b/apps/web/src/components/v2/diagnose-error-action.tsx
@@ -19,7 +19,7 @@ export function DiagnoseErrorAction({
diagnosis,
reasoningText,
}: DiagnoseErrorActionProps) {
- const [showReasoning, setShowReasoning] = useState(false);
+ const [showReasoning, setShowReasoning] = useState(true);
const getStatusIcon = () => {
switch (status) {
diff --git a/apps/web/src/components/v2/manager-chat.tsx b/apps/web/src/components/v2/manager-chat.tsx
index 6778f0fe..728dbf8f 100644
--- a/apps/web/src/components/v2/manager-chat.tsx
+++ b/apps/web/src/components/v2/manager-chat.tsx
@@ -12,6 +12,7 @@ import { useStream } from "@langchain/langgraph-sdk/react";
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
import { cn } from "@/lib/utils";
import { isAIMessageSDK } from "@/lib/langchain-messages";
+import { BasicMarkdownText } from "../thread/markdown-text";
function MessageCopyButton({ content }: { content: string }) {
const [copied, setCopied] = useState(false);
@@ -121,17 +122,17 @@ export function ManagerChat({
)}
-
+
{message.type === "human" ? "You" : "Agent"}
+
+
+
-
+
{messageContentString}
-
-
-
-
+
);
diff --git a/apps/web/src/components/v2/terminal-input.tsx b/apps/web/src/components/v2/terminal-input.tsx
index 6f3ba7ca..fbc1fe2a 100644
--- a/apps/web/src/components/v2/terminal-input.tsx
+++ b/apps/web/src/components/v2/terminal-input.tsx
@@ -145,7 +145,7 @@ export function TerminalInput({
return (
-
+
open-swe
@
github
diff --git a/apps/web/src/components/v2/thread-view-loading.tsx b/apps/web/src/components/v2/thread-view-loading.tsx
index 79259b97..321fb72e 100644
--- a/apps/web/src/components/v2/thread-view-loading.tsx
+++ b/apps/web/src/components/v2/thread-view-loading.tsx
@@ -66,7 +66,9 @@ export function LoadingActionsCard() {
-
+
+
+
);
}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 855c8879..74959ebd 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -18,7 +18,7 @@
},
"dependencies": {
"@langchain/core": "^0.3.56",
- "@langchain/langgraph": "^0.3.3",
+ "@langchain/langgraph": "^0.3.8",
"@langchain/langgraph-sdk": "^0.0.92",
"@octokit/rest": "^22.0.0",
"zod": "^3.25.32"
diff --git a/packages/shared/src/open-swe/reviewer/types.ts b/packages/shared/src/open-swe/reviewer/types.ts
new file mode 100644
index 00000000..2a19051b
--- /dev/null
+++ b/packages/shared/src/open-swe/reviewer/types.ts
@@ -0,0 +1,106 @@
+import "@langchain/langgraph/zod";
+import { z } from "zod";
+import {
+ Messages,
+ messagesStateReducer,
+ MessagesZodState,
+} from "@langchain/langgraph";
+import { CustomRules, TargetRepository, TaskPlan } from "../types.js";
+import { withLangGraph } from "@langchain/langgraph/zod";
+import { BaseMessage } from "@langchain/core/messages";
+
+export const ReviewerGraphStateObj = MessagesZodState.extend({
+ /**
+ * We must include the internal messages so that the reviewer has an
+ * accurate picture of the conversation.
+ */
+ internalMessages: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: messagesStateReducer,
+ },
+ jsonSchemaExtra: {
+ langgraph_type: "messages",
+ },
+ default: () => [],
+ }),
+ /**
+ * A separate list of messages for the reviewer. Used to track both
+ * internal messages which do not need to be shown to the user/propagated
+ * back to the programmer, and to determine how many reviewer actions have
+ * been executed.
+ */
+ reviewerMessages: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: messagesStateReducer,
+ },
+ jsonSchemaExtra: {
+ langgraph_type: "messages",
+ },
+ default: () => [],
+ }),
+ sandboxSessionId: withLangGraph(z.string(), {
+ reducer: {
+ schema: z.string(),
+ fn: (_state, update) => update,
+ },
+ }),
+ targetRepository: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: (_state, update) => update,
+ },
+ }),
+ githubIssueId: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: (_state, update) => update,
+ },
+ }),
+ codebaseTree: withLangGraph(z.string(), {
+ reducer: {
+ schema: z.string(),
+ fn: (_state, update) => update,
+ },
+ }),
+ taskPlan: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: (_state, update) => update,
+ },
+ }),
+ branchName: withLangGraph(z.string(), {
+ reducer: {
+ schema: z.string(),
+ fn: (_state, update) => update,
+ },
+ }),
+ baseBranchName: withLangGraph(z.string(), {
+ reducer: {
+ schema: z.string(),
+ fn: (_state, update) => update,
+ },
+ }),
+ changedFiles: withLangGraph(z.string(), {
+ reducer: {
+ schema: z.string(),
+ fn: (_state, update) => update,
+ },
+ }),
+ customRules: withLangGraph(z.custom().optional(), {
+ reducer: {
+ schema: z.custom().optional(),
+ fn: (_state, update) => update,
+ },
+ }),
+ dependenciesInstalled: withLangGraph(z.boolean(), {
+ reducer: {
+ schema: z.boolean(),
+ fn: (_state, update) => update,
+ },
+ }),
+});
+
+export type ReviewerGraphState = z.infer;
+export type ReviewerGraphUpdate = Partial;
diff --git a/packages/shared/src/open-swe/tools.ts b/packages/shared/src/open-swe/tools.ts
index c82dc1aa..b9ae1da2 100644
--- a/packages/shared/src/open-swe/tools.ts
+++ b/packages/shared/src/open-swe/tools.ts
@@ -106,113 +106,33 @@ export function createUpdatePlanToolFields() {
};
}
-export function createRgToolFields(targetRepository: TargetRepository) {
+export function createSearchToolFields(targetRepository: TargetRepository) {
const repoRoot = getRepoAbsolutePath(targetRepository);
- // Main ripgrep command schema
- const ripgrepCommandSchema = z.object({
+ const searchSchema = z.object({
pattern: z
.string()
+ .describe("The string or regex to search the codebase for."),
+ regex: z
+ .boolean()
.optional()
+ .default(false)
.describe(
- "The search pattern (regex). Leave empty when using flags like --files or --type-list",
- ),
-
- paths: z
- .array(z.string())
- .optional()
- .describe(
- "Files or directories to search. If empty, searches current directory",
- ),
-
- flags: z
- .array(z.string())
- .optional()
- .describe(
- 'Array of flags with their values. Examples: ["-i", "--type=rust", "-A", "3", "--files"]. Short flags like -i can be standalone, flags with values can be separate strings or use = for long flags',
- ),
- });
-
- return {
- name: "rg",
- schema: ripgrepCommandSchema,
- description: `Call this tool to run the rg command (ripgrep). This should ONLY be called if you want to search for files in the repository. The working directory this command will be executed in is \`${repoRoot}\`.`,
- };
-}
-
-// Only used for type inference
-const _tmpRgToolSchema = createRgToolFields({ owner: "x", repo: "x" }).schema;
-export type RipgrepCommand = z.infer;
-
-export function formatRgCommand(
- cmd: RipgrepCommand,
- options?: {
- excludeRequiredFlags?: boolean;
- },
-): string[] {
- const args = ["rg"];
-
- // Always include these flags
- const requiredFlags = ["--color", "never", "--line-number", "--heading"];
-
- // Add user-provided flags, ensuring we don't duplicate the required ones
- if (cmd.flags) {
- // Filter out any flags that would duplicate our required flags
- const filteredFlags = cmd.flags.filter((flag) => {
- // Check for exact matches or flags that start with our required prefixes
- return (
- !requiredFlags.includes(flag) &&
- !flag.startsWith("--color=") &&
- flag !== "--line-number" &&
- flag !== "-n" &&
- flag !== "--heading"
- );
- });
-
- args.push(...filteredFlags);
- }
-
- if (!options?.excludeRequiredFlags) {
- // Add the required flags
- args.push(...requiredFlags);
- }
-
- if (cmd.pattern) {
- args.push(cmd.pattern);
- }
-
- if (cmd.paths) {
- args.push(...cmd.paths);
- }
-
- return args;
-}
-
-export function createFindInstancesOfToolFields(
- targetRepository: TargetRepository,
-) {
- const repoRoot = getRepoAbsolutePath(targetRepository);
- const findInstancesOfSchema = z.object({
- query: z
- .string()
- .describe(
- "The query/keyword to search for. This should be a literal string, not a regex.",
+ "Whether or not to treat the pattern as a regex. Defaults to false.",
),
case_sensitive: z
.boolean()
.optional()
- .default(true)
+ .default(false)
.describe(
- "Whether or not to make the query search case sensitive. Defaults to true",
+ "Whether or not to make the search case sensitive. Defaults to false.",
),
- match_word: z
- .boolean()
+ context_lines: z
+ .number()
.optional()
- .default(true)
- .describe(
- "Whether or not to only show results which match the exact keyword. Defaults to true",
- ),
+ .default(0)
+ .describe("Number of lines of context to include before/after matches."),
exclude_files: z
.string()
@@ -223,15 +143,109 @@ export function createFindInstancesOfToolFields(
.string()
.optional()
.describe("Glob pattern of files to include"),
+
+ max_results: z
+ .number()
+ .optional()
+ .default(0)
+ .describe(
+ "Maximum number of results to return. Defaults to 0, which returns all results.",
+ ),
+ file_types: z
+ .array(z.string())
+ .optional()
+ .describe("Restrict to certain file extensions (e.g., ['.js', '.ts'])."),
+ follow_symlinks: z
+ .boolean()
+ .optional()
+ .default(false)
+ .describe("Whether or not to follow symlinks. Defaults to false."),
});
return {
- name: "find_instances_of",
- schema: findInstancesOfSchema,
- description: `Find all instances of a string in the repository. Returns results with 3 lines of context above and below each match, absolute file paths, and total result count. The working directory this command will be executed in is \`${repoRoot}\`.`,
+ name: "search",
+ schema: searchSchema,
+ description: `Execute a search in the repository. The working directory this command will be executed in is \`${repoRoot}\`.`,
};
}
+// Only used for type inference
+const _tmpSearchToolSchema = createSearchToolFields({
+ owner: "x",
+ repo: "x",
+}).schema;
+export type SearchCommand = z.infer;
+
+function escapeShellArg(arg: string): string {
+ // If the string contains a single quote, close the string, escape the single quote, and reopen it
+ // Example: foo'bar → 'foo'\''bar'
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
+}
+
+export function formatSearchCommand(
+ cmd: SearchCommand,
+ options?: {
+ excludeRequiredFlags?: boolean;
+ },
+): string[] {
+ const args = ["rg"];
+
+ // Required flags to keep formatting and output consistent
+ const requiredFlags = ["--color=never", "--line-number", "--heading"];
+
+ if (!options?.excludeRequiredFlags) {
+ args.push(...requiredFlags);
+ }
+
+ // Case sensitivity
+ if (!cmd.case_sensitive) {
+ args.push("-i");
+ }
+
+ // Regex vs fixed string
+ if (!cmd.regex) {
+ args.push("--fixed-strings");
+ }
+
+ // Context lines
+ if (cmd.context_lines && cmd.context_lines > 0) {
+ args.push(`-C`, String(cmd.context_lines));
+ }
+
+ // File globs
+ if (cmd.include_files) {
+ args.push("--glob", cmd.include_files);
+ }
+
+ if (cmd.exclude_files) {
+ args.push("--glob", `!${cmd.exclude_files}`);
+ }
+
+ // File types
+ if (cmd.file_types && cmd.file_types.length > 0) {
+ for (const ext of cmd.file_types) {
+ args.push("--glob", `**/*${ext}`);
+ }
+ }
+
+ // Follow symlinks
+ if (cmd.follow_symlinks) {
+ args.push("-L");
+ }
+
+ // Max results (0 = unlimited)
+ if (cmd.max_results && cmd.max_results > 0) {
+ args.push("--max-count", String(cmd.max_results));
+ }
+
+ // The pattern (must come after flags)
+ if (cmd.pattern) {
+ args.push(escapeShellArg(cmd.pattern));
+ }
+
+ return args;
+}
+
export function createMarkTaskNotCompletedToolFields() {
const markTaskNotCompletedToolSchema = z.object({
reasoning: z
@@ -396,3 +410,42 @@ export function createConversationHistorySummaryToolFields() {
schema: conversationHistorySummarySchema,
};
}
+
+export function createCodeReviewMarkTaskCompletedFields() {
+ const markTaskCompletedSchema = z.object({
+ review: z
+ .string()
+ .describe(
+ "Your final review for the completed task. This should be concise, but descriptive.",
+ ),
+ });
+
+ return {
+ name: "code_review_mark_task_completed",
+ schema: markTaskCompletedSchema,
+ description:
+ "Use this tool to mark a task as completed. This should be called if you determine that the task has been successfully completed.",
+ };
+}
+
+export function createCodeReviewMarkTaskNotCompleteFields() {
+ const markTaskNotCompleteSchema = z.object({
+ review: z
+ .string()
+ .describe(
+ "Your final review for the completed task. This should be concise, but descriptive.",
+ ),
+ additional_actions: z
+ .array(z.string())
+ .describe(
+ "A list of additional actions to take which will successfully satisfy your review, and complete the task.",
+ ),
+ });
+
+ return {
+ name: "code_review_mark_task_not_complete",
+ schema: markTaskNotCompleteSchema,
+ description:
+ "Use this tool to mark a task as not complete. This should be called if you determine that the task has not been successfully completed, and you have additional tasks the programmer should take to successfully complete the task.",
+ };
+}
diff --git a/packages/shared/src/open-swe/types.ts b/packages/shared/src/open-swe/types.ts
index c8c13ff0..304f11fa 100644
--- a/packages/shared/src/open-swe/types.ts
+++ b/packages/shared/src/open-swe/types.ts
@@ -240,6 +240,16 @@ export const GraphAnnotation = MessagesZodState.extend({
fn: (_state, update) => update,
},
}),
+ /**
+ * The review generated by the reviewer subgraph
+ */
+ review: withLangGraph(z.custom(), {
+ reducer: {
+ schema: z.custom(),
+ fn: (_state, update) => update,
+ },
+ default: () => "",
+ }),
// ---NOT USED---
ui: z
@@ -272,6 +282,15 @@ export const GraphConfigurationMetadata: {
"Maximum number of context gathering actions during planning",
},
},
+ maxReviewActions: {
+ x_open_swe_ui_config: {
+ type: "number",
+ default: 30,
+ min: 1,
+ max: 250,
+ description: "Maximum number of review actions during planning",
+ },
+ },
plannerModelName: {
x_open_swe_ui_config: {
type: "select",
@@ -441,6 +460,16 @@ export const GraphConfiguration = z.object({
.number()
.optional()
.langgraph.metadata(GraphConfigurationMetadata.maxContextActions),
+ /**
+ * The maximum number of context gathering actions to take during review.
+ * Each action consists of 2 messages (request & result), plus 1 human message.
+ * Total messages = maxReviewActions * 2 + 1
+ * @default 30
+ */
+ maxReviewActions: z
+ .number()
+ .optional()
+ .langgraph.metadata(GraphConfigurationMetadata.maxReviewActions),
/**
* The model ID to use for the planning step.
diff --git a/yarn.lock b/yarn.lock
index 55525c32..41af88f7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2597,12 +2597,12 @@ __metadata:
languageName: node
linkType: hard
-"@langchain/langgraph@npm:^0.3.3":
- version: 0.3.7
- resolution: "@langchain/langgraph@npm:0.3.7"
+"@langchain/langgraph@npm:^0.3.8":
+ version: 0.3.8
+ resolution: "@langchain/langgraph@npm:0.3.8"
dependencies:
"@langchain/langgraph-checkpoint": ~0.0.18
- "@langchain/langgraph-sdk": ~0.0.90
+ "@langchain/langgraph-sdk": ~0.0.92
uuid: ^10.0.0
zod: ^3.25.32
peerDependencies:
@@ -2611,7 +2611,7 @@ __metadata:
peerDependenciesMeta:
zod-to-json-schema:
optional: true
- checksum: 6a924940d92d4c0c97c18665a1d1f70bfa83c2d5f8b221ea6fe90d3d47bfa6518a6032058bb9b722199ce0c9fb511e9fe2818e2d877ffb7c43510715c1b82d84
+ checksum: 08089b5f151d43b556dba604699c52cb4d323e1fc17ab24572429db582c90ad2da477f90070b2812b03e6aec9d080c09a292c420943b65aeaba858cdf0d12e3f
languageName: node
linkType: hard
@@ -3461,7 +3461,7 @@ __metadata:
"@langchain/community": ^0.3.47
"@langchain/core": ^0.3.56
"@langchain/google-genai": ^0.2.9
- "@langchain/langgraph": ^0.3.3
+ "@langchain/langgraph": ^0.3.8
"@langchain/langgraph-cli": latest
"@langchain/langgraph-sdk": ^0.0.92
"@langchain/mcp-adapters": ^0.5.2
@@ -3512,7 +3512,7 @@ __metadata:
"@eslint/eslintrc": ^3.1.0
"@eslint/js": ^9.19.0
"@langchain/core": ^0.3.56
- "@langchain/langgraph": ^0.3.3
+ "@langchain/langgraph": ^0.3.8
"@langchain/langgraph-sdk": ^0.0.92
"@octokit/rest": ^22.0.0
"@octokit/types": ^12.0.0
@@ -3537,7 +3537,7 @@ __metadata:
dependencies:
"@eslint/js": ^9.19.0
"@langchain/core": ^0.3.57
- "@langchain/langgraph": ^0.3.3
+ "@langchain/langgraph": ^0.3.8
"@langchain/langgraph-sdk": ^0.0.92
"@octokit/app": ^16.0.1
"@octokit/types": ^14.1.0