diff --git a/apps/web/public/favicon.png b/apps/web/public/favicon.png
deleted file mode 100644
index 38454444..00000000
Binary files a/apps/web/public/favicon.png and /dev/null differ
diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico
index 2a7a6db0..f900fc75 100644
Binary files a/apps/web/src/app/favicon.ico and b/apps/web/src/app/favicon.ico differ
diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx
index 15644544..24e3da18 100644
--- a/apps/web/src/components/gen-ui/action-step.tsx
+++ b/apps/web/src/components/gen-ui/action-step.tsx
@@ -1,6 +1,5 @@
"use client";
-import "../app/globals.css";
import { useState } from "react";
import {
Terminal,
@@ -25,7 +24,7 @@ type BaseActionProps = {
// Shell command specific props
type ShellActionProps = BaseActionProps & {
actionType: "shell";
- command: string;
+ command: string[];
workdir?: string;
output?: string;
errorCode?: number;
@@ -41,7 +40,7 @@ type PatchActionProps = BaseActionProps & {
};
// Union type for all possible action props
-type ActionStepProps =
+export type ActionStepProps =
| (BaseActionProps & { status: "loading" })
| ShellActionProps
| PatchActionProps;
@@ -136,7 +135,7 @@ export function ActionStep(props: ActionStepProps) {
)}
- {props.command}
+ {props.command.join(" ")}
);
diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx
index 1b735b35..56c901c4 100644
--- a/apps/web/src/components/thread/messages/ai.tsx
+++ b/apps/web/src/components/thread/messages/ai.tsx
@@ -1,6 +1,11 @@
import { parsePartialJson } from "@langchain/core/output_parsers";
import { useStreamContext } from "@/providers/Stream";
-import { AIMessage, Checkpoint, Message } from "@langchain/langgraph-sdk";
+import {
+ AIMessage,
+ Checkpoint,
+ Message,
+ ToolMessage,
+} from "@langchain/langgraph-sdk";
import { getContentString } from "../utils";
import { BranchSwitcher, CommandBar } from "./shared";
import { MarkdownText } from "../markdown-text";
@@ -12,6 +17,24 @@ import { Fragment } from "react/jsx-runtime";
import { useQueryState, parseAsBoolean } from "nuqs";
import { useArtifact } from "../artifact";
import { Interrupt } from "./interrupt";
+import {
+ ActionStep,
+ type ActionStepProps,
+} from "@/components/gen-ui/action-step";
+import { ToolCall } from "@langchain/core/messages/tool";
+import {
+ createApplyPatchToolFields,
+ createShellToolFields,
+} from "@open-swe/shared/open-swe/tools";
+import { z } from "zod";
+import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
+
+// Used only for Zod type inference.
+const dummyRepo = { owner: "dummy", repo: "dummy" };
+const shellTool = createShellToolFields(dummyRepo);
+type ShellToolArgs = z.infer;
+const applyPatchTool = createApplyPatchToolFields(dummyRepo);
+type ApplyPatchToolArgs = z.infer;
function CustomComponent({
message,
@@ -65,6 +88,54 @@ function parseAnthropicStreamedToolCalls(
});
}
+export function mapToolMessageToActionStepProps(
+ message: ToolMessage,
+ thread: { messages: Message[] },
+): ActionStepProps {
+ const toolCall: ToolCall | undefined = thread.messages
+ .filter(isAIMessageSDK)
+ .flatMap((m) => m.tool_calls ?? [])
+ .find((tc) => tc.id === message.tool_call_id);
+
+ const aiMessage = thread.messages
+ .filter(isAIMessageSDK)
+ .find((m) => m.tool_calls?.some((tc) => tc.id === message.tool_call_id));
+ const reasoningText = aiMessage
+ ? getContentString(aiMessage.content)
+ : undefined;
+
+ const status: ActionStepProps["status"] = "done";
+ const success = message.status === "success";
+
+ if (toolCall?.name === shellTool.name) {
+ const args = toolCall.args as ShellToolArgs;
+ return {
+ actionType: shellTool.name as "shell",
+ status,
+ success,
+ command: args.command || [],
+ workdir: args.workdir,
+ output: getContentString(message.content),
+ reasoningText,
+ };
+ } else if (toolCall?.name === applyPatchTool.name) {
+ const args = toolCall.args as ApplyPatchToolArgs;
+ return {
+ actionType: "apply-patch",
+ status,
+ success,
+ file: args.file_path || "",
+ diff: args.diff,
+ reasoningText,
+ errorMessage: !success ? getContentString(message.content) : undefined,
+ };
+ }
+ return {
+ status: "loading",
+ summaryText: reasoningText,
+ };
+}
+
export function AssistantMessage({
message,
isLoading,
@@ -84,18 +155,121 @@ export function AssistantMessage({
);
const thread = useStreamContext();
+ const messages = thread.messages;
+ const idx = message ? messages.findIndex((m) => m.id === message.id) : -1;
+ const nextMessage = idx >= 0 ? messages[idx + 1] : undefined;
+
+ const meta = message ? thread.getMessagesMetadata(message) : undefined;
+ const threadInterrupt = thread.interrupt;
+ const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
+ const anthropicStreamedToolCalls = Array.isArray(content)
+ ? parseAnthropicStreamedToolCalls(content)
+ : undefined;
+
+ // Helper: get tool call name from AI message (OpenAI or Anthropic)
+ const aiToolCallName = (() => {
+ if (message && isAIMessageSDK(message)) {
+ return message.tool_calls?.[0]?.name;
+ }
+ if (anthropicStreamedToolCalls?.length) {
+ return anthropicStreamedToolCalls[anthropicStreamedToolCalls.length - 1]
+ .name;
+ }
+ return undefined;
+ })();
+
+ const aiToolCallArgs = (() => {
+ if (message && isAIMessageSDK(message)) {
+ return message.tool_calls?.[0]?.args;
+ }
+ if (anthropicStreamedToolCalls?.length) {
+ return anthropicStreamedToolCalls[anthropicStreamedToolCalls.length - 1]
+ .args;
+ }
+ return undefined;
+ })();
+
+ const toolResult =
+ nextMessage &&
+ isToolMessageSDK(nextMessage) &&
+ aiToolCallName &&
+ nextMessage.tool_call_id ===
+ (message && isAIMessageSDK(message)
+ ? message.tool_calls?.[0]?.id
+ : anthropicStreamedToolCalls?.length
+ ? anthropicStreamedToolCalls[anthropicStreamedToolCalls.length - 1].id
+ : undefined)
+ ? nextMessage
+ : undefined;
+
+ if (
+ message &&
+ (aiToolCallName === shellTool.name ||
+ aiToolCallName === applyPatchTool.name)
+ ) {
+ if (toolResult) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ if (
+ message?.type === "tool" &&
+ (message.name === shellTool.name || message.name === applyPatchTool.name) &&
+ idx > 0 &&
+ messages[idx - 1] &&
+ ((messages[idx - 1] &&
+ isAIMessageSDK(messages[idx - 1]) &&
+ (messages[idx - 1] as AIMessage).tool_calls?.some(
+ (tc) =>
+ tc.id === (message as ToolMessage).tool_call_id &&
+ (tc.name === shellTool.name || tc.name === applyPatchTool.name),
+ )) ||
+ (Array.isArray(messages[idx - 1].content) &&
+ parseAnthropicStreamedToolCalls(
+ messages[idx - 1].content as MessageContentComplex[],
+ )?.some(
+ (tc) =>
+ tc.id === (message as ToolMessage).tool_call_id &&
+ (tc.name === shellTool.name || tc.name === applyPatchTool.name),
+ )))
+ ) {
+ return null;
+ }
+
const isLastMessage =
thread.messages[thread.messages.length - 1].id === message?.id;
const hasNoAIOrToolMessages = !thread.messages.find(
(m) => m.type === "ai" || m.type === "tool",
);
- const meta = message ? thread.getMessagesMetadata(message) : undefined;
- const threadInterrupt = thread.interrupt;
-
- const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint;
- const anthropicStreamedToolCalls = Array.isArray(content)
- ? parseAnthropicStreamedToolCalls(content)
- : undefined;
const hasToolCalls =
message &&
diff --git a/apps/web/src/lib/langchain-messages.ts b/apps/web/src/lib/langchain-messages.ts
new file mode 100644
index 00000000..96634242
--- /dev/null
+++ b/apps/web/src/lib/langchain-messages.ts
@@ -0,0 +1,14 @@
+import {
+ AIMessage,
+ HumanMessage,
+ Message,
+ ToolMessage,
+} from "@langchain/langgraph-sdk";
+
+export function isAIMessageSDK(m: Message): m is AIMessage {
+ return m.type === "ai";
+}
+
+export function isToolMessageSDK(m: Message): m is ToolMessage {
+ return m.type === "tool";
+}