diff --git a/README.md b/README.md index d2e02fc9..9bb64eee 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,13 @@ Once you've accepted the plan, it will begin the execution flow. When the agent Open SWE will automatically create a branch whenever you create a new thread with a naming format of `open-swe/`. Every time a file is created, modified, or deleted, the changes will be committed to this branch. You can access the changes in the repository by checking out this branch. + + +## Install Daytona CLI +```bash +# Mac os +brew install daytonaio/cli/daytona +# Windows +powershell -Command "irm https://get.daytona.io/windows | iex" + +``` \ No newline at end of file diff --git a/apps/open-swe/package.json b/apps/open-swe/package.json index 2e591258..d689237b 100644 --- a/apps/open-swe/package.json +++ b/apps/open-swe/package.json @@ -10,6 +10,7 @@ "type": "module", "scripts": { "dev": "langgraphjs dev --no-browser --config ../../langgraph.json", + "clean": "rm -rf ../../.langgraph_api || true && rm -rf .turbo || true && rm -rf ./dist || true", "build": "tsc", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -24,8 +25,8 @@ "@langchain/anthropic": "^0.3.20", "@langchain/core": "^0.3.56", "@langchain/google-genai": "^0.2.9", - "@langchain/langgraph": "^0.2.73", - "@langchain/langgraph-sdk": "^0.0.78", + "@langchain/langgraph": "^0.3.1", + "@langchain/langgraph-sdk": "^0.0.83", "@langchain/openai": "^0.5.10", "@octokit/rest": "^22.0.0", "@open-swe/shared": "workspace:*", diff --git a/apps/open-swe/src/nodes/diagnose-error.ts b/apps/open-swe/src/nodes/diagnose-error.ts index 6a723fad..9cd7f451 100644 --- a/apps/open-swe/src/nodes/diagnose-error.ts +++ b/apps/open-swe/src/nodes/diagnose-error.ts @@ -10,10 +10,8 @@ import { PlanItem, } from "@open-swe/shared/open-swe/types"; import { formatPlanPromptWithSummaries } from "../utils/plan-prompt.js"; -import { - getMessageContentString, - getMessageString, -} from "../utils/message/content.js"; +import { getMessageString } from "../utils/message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; import { loadModel, Task } from "../utils/load-model.js"; import { z } from "zod"; import { createLogger, LogLevel } from "../utils/logger.js"; diff --git a/apps/open-swe/src/nodes/generate-conclusion.ts b/apps/open-swe/src/nodes/generate-conclusion.ts index dbd1394f..1d6acc71 100644 --- a/apps/open-swe/src/nodes/generate-conclusion.ts +++ b/apps/open-swe/src/nodes/generate-conclusion.ts @@ -5,10 +5,8 @@ import { PlanItem, } from "@open-swe/shared/open-swe/types"; import { loadModel, Task } from "../utils/load-model.js"; -import { - getMessageContentString, - getMessageString, -} from "../utils/message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; +import { getMessageString } from "../utils/message/content.js"; import { createLogger, LogLevel } from "../utils/logger.js"; import { getUserRequest } from "../utils/user-request.js"; import { diff --git a/apps/open-swe/src/nodes/generate-message.ts b/apps/open-swe/src/nodes/generate-message.ts index 4f7f3afa..1562af45 100644 --- a/apps/open-swe/src/nodes/generate-message.ts +++ b/apps/open-swe/src/nodes/generate-message.ts @@ -14,7 +14,7 @@ import { formatPlanPrompt } from "../utils/plan-prompt.js"; import { stopSandbox } from "../utils/sandbox.js"; import { createLogger, LogLevel } from "../utils/logger.js"; import { getCurrentPlanItem } from "../utils/current-task.js"; -import { getMessageContentString } from "../utils/message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; import { getActivePlanItems } from "../utils/task-plan.js"; import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants"; diff --git a/apps/open-swe/src/nodes/interrupt-plan.ts b/apps/open-swe/src/nodes/interrupt-plan.ts index 44169f3a..2b9327f2 100644 --- a/apps/open-swe/src/nodes/interrupt-plan.ts +++ b/apps/open-swe/src/nodes/interrupt-plan.ts @@ -8,6 +8,10 @@ import { import { startSandbox } from "../utils/sandbox.js"; import { createNewTask } from "../utils/task-plan.js"; import { getUserRequest } from "../utils/user-request.js"; +import { + PLAN_INTERRUPT_ACTION_TITLE, + PLAN_INTERRUPT_DELIMITER, +} from "@open-swe/shared/constants"; export async function interruptPlan(state: GraphState): Promise { const { proposedPlan } = state; @@ -17,9 +21,9 @@ export async function interruptPlan(state: GraphState): Promise { const interruptRes = interrupt({ action_request: { - action: "Approve/Edit Plan", + action: PLAN_INTERRUPT_ACTION_TITLE, args: { - plan: proposedPlan.join("\n:::\n"), + plan: proposedPlan.join(`\n${PLAN_INTERRUPT_DELIMITER}\n`), }, }, config: { @@ -29,7 +33,7 @@ export async function interruptPlan(state: GraphState): Promise { allow_ignore: true, }, description: `A new plan has been generated for your request. Please review it and either approve it, edit it, respond to it, or ignore it. Responses will be passed to an LLM where it will rewrite then plan. - If editing the plan, ensure each step in the plan is separated by ":::".`, + If editing the plan, ensure each step in the plan is separated by "${PLAN_INTERRUPT_DELIMITER}".`, })[0]; if (!state.sandboxSessionId) { @@ -66,7 +70,7 @@ export async function interruptPlan(state: GraphState): Promise { // Plan was edited, route to the generate-action node to start taking actions. const editedPlan = (interruptRes.args as ActionRequest).args.plan - .split(":::") + .split(PLAN_INTERRUPT_DELIMITER) .map((step: string) => step.trim()); const planItems = editedPlan.map((p: string, index: number) => ({ diff --git a/apps/open-swe/src/nodes/summarize-task-steps.ts b/apps/open-swe/src/nodes/summarize-task-steps.ts index d934277d..9f8fc8e3 100644 --- a/apps/open-swe/src/nodes/summarize-task-steps.ts +++ b/apps/open-swe/src/nodes/summarize-task-steps.ts @@ -9,10 +9,8 @@ import { loadModel, Task } from "../utils/load-model.js"; import { AIMessage, BaseMessage } from "@langchain/core/messages"; import { formatPlanPrompt } from "../utils/plan-prompt.js"; import { createLogger, LogLevel } from "../utils/logger.js"; -import { - getMessageContentString, - getMessageString, -} from "../utils/message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; +import { getMessageString } from "../utils/message/content.js"; import { removeLastTaskMessages } from "../utils/message/modify-array.js"; import { Command } from "@langchain/langgraph"; import { ConfigurableModel } from "langchain/chat_models/universal"; diff --git a/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts b/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts index 1e66ae00..9e1ad72f 100644 --- a/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts +++ b/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts @@ -3,7 +3,7 @@ import { shellTool } from "../../../tools/index.js"; import { PlannerGraphState, PlannerGraphUpdate } from "../types.js"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { createLogger, LogLevel } from "../../../utils/logger.js"; -import { getMessageContentString } from "../../../utils/message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; import { getUserRequest } from "../../../utils/user-request.js"; import { isHumanMessage } from "@langchain/core/messages"; import { formatFollowupMessagePrompt } from "../utils/followup-prompt.js"; diff --git a/apps/open-swe/src/utils/git.ts b/apps/open-swe/src/utils/git.ts index ac375213..9311153a 100644 --- a/apps/open-swe/src/utils/git.ts +++ b/apps/open-swe/src/utils/git.ts @@ -306,7 +306,7 @@ export async function configureGitUserInRepo( } // Set user email - use fetched email or fallback to a generic noreply address - const emailToUse = userEmail || "open-swe-bot@noreply.github.com"; + const emailToUse = userEmail || `${repo}-bot@noreply.github.com`; const configUserEmailOutput = await sandbox.process.executeCommand( `git config user.email "${emailToUse}"`, absoluteRepoDir, diff --git a/apps/open-swe/src/utils/message/content.ts b/apps/open-swe/src/utils/message/content.ts index e4ef6159..65c1b1c9 100644 --- a/apps/open-swe/src/utils/message/content.ts +++ b/apps/open-swe/src/utils/message/content.ts @@ -6,20 +6,11 @@ import { isHumanMessage, isSystemMessage, isToolMessage, - MessageContent, SystemMessage, ToolMessage, } from "@langchain/core/messages"; import { ToolCall } from "@langchain/core/messages/tool"; - -export function getMessageContentString(content: MessageContent): string { - if (typeof content === "string") return content; - - return content - .filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(" "); -} +import { getMessageContentString } from "@open-swe/shared/messages"; export function getToolCallsString(toolCalls: ToolCall[] | undefined): string { if (!toolCalls?.length) return ""; diff --git a/apps/open-swe/src/utils/user-request.ts b/apps/open-swe/src/utils/user-request.ts index 4ee786a5..23c3a079 100644 --- a/apps/open-swe/src/utils/user-request.ts +++ b/apps/open-swe/src/utils/user-request.ts @@ -3,7 +3,7 @@ import { isHumanMessage, HumanMessage, } from "@langchain/core/messages"; -import { getMessageContentString } from "./message/content.js"; +import { getMessageContentString } from "@open-swe/shared/messages"; export function getUserRequest( messages: BaseMessage[], diff --git a/apps/web/package.json b/apps/web/package.json index ffb8824d..5dc3280d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,16 +20,22 @@ }, "dependencies": { "@langchain/core": "^0.3.57", - "@langchain/langgraph": "^0.2.73", - "@langchain/langgraph-sdk": "^0.0.78", + "@langchain/langgraph": "^0.3.1", + "@langchain/langgraph-sdk": "^0.0.83", + "@open-swe/shared": "workspace:*", + "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.1.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -58,7 +64,8 @@ "tailwindcss-animate": "^1.0.7", "use-stick-to-bottom": "^1.0.46", "uuid": "^11.1.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.5" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/apps/web/public/favicon.png b/apps/web/public/favicon.png new file mode 100644 index 00000000..38454444 Binary files /dev/null and b/apps/web/public/favicon.png differ diff --git a/apps/web/src/app/api/github/installation-callback/route.ts b/apps/web/src/app/api/github/installation-callback/route.ts index 5ed0ed92..234a48cf 100644 --- a/apps/web/src/app/api/github/installation-callback/route.ts +++ b/apps/web/src/app/api/github/installation-callback/route.ts @@ -13,7 +13,6 @@ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const installationId = searchParams.get("installation_id"); - const setupAction = searchParams.get("setup_action"); // Get the return URL from cookies const returnTo = diff --git a/apps/web/src/app/api/github/installation/callback/route.ts b/apps/web/src/app/api/github/installation/callback/route.ts index 63d66023..30045f65 100644 --- a/apps/web/src/app/api/github/installation/callback/route.ts +++ b/apps/web/src/app/api/github/installation/callback/route.ts @@ -9,7 +9,6 @@ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const installationId = searchParams.get("installation_id"); - const setupAction = searchParams.get("setup_action"); if (!installationId) { return NextResponse.redirect( diff --git a/apps/web/src/app/github/page.tsx b/apps/web/src/app/github/page.tsx index b28a5f17..c033a186 100644 --- a/apps/web/src/app/github/page.tsx +++ b/apps/web/src/app/github/page.tsx @@ -25,7 +25,7 @@ export default function GitHubPage() { setError(errorData.error); } } - } catch (err) { + } catch { setError("Failed to check installation status"); } }; @@ -50,7 +50,7 @@ export default function GitHubPage() { const errorData = await response.json(); setError(errorData.error); } - } catch (err) { + } catch { setError("Failed to fetch repositories"); } finally { setLoading(false); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 37d29c74..a8ff7e4c 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -13,6 +13,11 @@ const inter = Inter({ export const metadata: Metadata = { title: "Open SWE", description: "Open SWE UX by LangChain", + icons: { + icon: "/favicon.png", + shortcut: "/favicon.png", + apple: "/favicon.png", + }, }; export default function RootLayout({ diff --git a/apps/web/src/components/configuration-sidebar/config-field.tsx b/apps/web/src/components/configuration-sidebar/config-field.tsx new file mode 100644 index 00000000..2c78968d --- /dev/null +++ b/apps/web/src/components/configuration-sidebar/config-field.tsx @@ -0,0 +1,291 @@ +"use client"; + +import { useState } from "react"; +import { Slider } from "@/components/ui/slider"; +import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { useConfigStore } from "@/hooks/use-config-store"; +import { AlertCircle } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import _ from "lodash"; +import { cn } from "@/lib/utils"; + +interface Option { + label: string; + value: string; +} + +interface ConfigFieldProps { + id: string; + label: string; + type: + | "text" + | "textarea" + | "number" + | "switch" + | "slider" + | "select" + | "json"; + description?: string; + placeholder?: string; + options?: Option[]; + min?: number; + max?: number; + step?: number; + className?: string; + // Optional props for external state management + value?: any; + setValue?: (value: any) => void; +} + +export function ConfigField({ + id, + label, + type, + description, + placeholder, + options = [], + min, + max, + step = 1, + className, + value: externalValue, // TODO: Rename to avoid conflict + setValue: externalSetValue, // TODO: Rename to avoid conflict +}: ConfigFieldProps) { + const store = useConfigStore(); + const [jsonError, setJsonError] = useState(null); + + // Determine whether to use external state or Zustand store + const isExternallyManaged = externalSetValue !== undefined; + + const currentValue = isExternallyManaged + ? externalValue + : store.configs[id]?.[id]; + + const handleChange = (newValue: any) => { + setJsonError(null); + if (isExternallyManaged && externalSetValue) { + externalSetValue(newValue); + } else { + store.updateConfig(id, newValue); + } + }; + + const handleJsonChange = (jsonString: string) => { + try { + if (!jsonString.trim()) { + handleChange(undefined); + setJsonError(null); + return; + } + + const parsedJson = JSON.parse(jsonString); + + handleChange(parsedJson); + setJsonError(null); + } catch { + if (isExternallyManaged && externalSetValue) { + externalSetValue(jsonString); + } else { + store.updateConfig(id, jsonString); + } + setJsonError("Invalid JSON format"); + } + }; + + const handleFormatJson = (jsonString: string) => { + try { + const parsed = JSON.parse(jsonString); + handleChange(parsed); + setJsonError(null); + } catch { + setJsonError("Invalid JSON format"); + } + }; + + return ( +
+
+ + {type === "switch" && ( + + )} +
+ + {description && ( +

+ {description} +

+ )} + + {type === "text" && ( + handleChange(e.target.value)} + placeholder={placeholder} + /> + )} + + {type === "textarea" && ( +