mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
fix: Improve plan interrupt UI (#167)
* fix: Improve plan interrupt UI * format * remove old interrupt code * cr * bump dep * bump deps * fix deps * cr * cr * cr
This commit is contained in:
parent
b25a29f126
commit
3314821e19
14 changed files with 375 additions and 182 deletions
|
|
@ -26,7 +26,7 @@
|
||||||
"@langchain/anthropic": "^0.3.20",
|
"@langchain/anthropic": "^0.3.20",
|
||||||
"@langchain/core": "^0.3.56",
|
"@langchain/core": "^0.3.56",
|
||||||
"@langchain/google-genai": "^0.2.9",
|
"@langchain/google-genai": "^0.2.9",
|
||||||
"@langchain/langgraph": "^0.3.1",
|
"@langchain/langgraph": "^0.3.3",
|
||||||
"@langchain/langgraph-sdk": "^0.0.83",
|
"@langchain/langgraph-sdk": "^0.0.83",
|
||||||
"@langchain/openai": "^0.5.10",
|
"@langchain/openai": "^0.5.10",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"langchain": "^0.3.26",
|
"langchain": "^0.3.26",
|
||||||
"langsmith": "^0.3.29",
|
"langsmith": "^0.3.29",
|
||||||
"uuid": "^11.0.5",
|
"uuid": "^11.0.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.25.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,16 @@ import { GraphAnnotation } from "@open-swe/shared/open-swe/types";
|
||||||
import { withLangGraph } from "@langchain/langgraph/zod";
|
import { withLangGraph } from "@langchain/langgraph/zod";
|
||||||
|
|
||||||
export const PlannerGraphStateObj = GraphAnnotation.extend({
|
export const PlannerGraphStateObj = GraphAnnotation.extend({
|
||||||
plannerMessages: withLangGraph<BaseMessage[], Messages>(
|
plannerMessages: withLangGraph(z.custom<BaseMessage[]>(), {
|
||||||
z.custom<BaseMessage[]>(),
|
reducer: {
|
||||||
{
|
schema: z.custom<Messages>(),
|
||||||
reducer: {
|
fn: messagesStateReducer,
|
||||||
schema: z.custom<Messages>(),
|
|
||||||
fn: messagesStateReducer,
|
|
||||||
},
|
|
||||||
jsonSchemaExtra: {
|
|
||||||
langgraph_type: "messages",
|
|
||||||
},
|
|
||||||
default: () => [],
|
|
||||||
},
|
},
|
||||||
),
|
jsonSchemaExtra: {
|
||||||
|
langgraph_type: "messages",
|
||||||
|
},
|
||||||
|
default: () => [],
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PlannerGraphState = z.infer<typeof PlannerGraphStateObj>;
|
export type PlannerGraphState = z.infer<typeof PlannerGraphStateObj>;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/core": "^0.3.57",
|
"@langchain/core": "^0.3.57",
|
||||||
"@langchain/langgraph": "^0.3.1",
|
"@langchain/langgraph": "^0.3.3",
|
||||||
"@langchain/langgraph-sdk": "^0.0.83",
|
"@langchain/langgraph-sdk": "^0.0.83",
|
||||||
"@open-swe/shared": "*",
|
"@open-swe/shared": "*",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"use-stick-to-bottom": "^1.0.46",
|
"use-stick-to-bottom": "^1.0.46",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.25.32",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,26 @@
|
||||||
import { Check, Clock, Play } from "lucide-react";
|
import { Check, Clock, Pencil, Play, Trash } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { TooltipIconButton } from "../ui/tooltip-icon-button";
|
||||||
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface PlanViewerProps {
|
interface PlanViewerProps {
|
||||||
planItems: PlanItem[];
|
planItems: PlanItem[];
|
||||||
className?: string;
|
className?: string;
|
||||||
isProposedPlan?: boolean;
|
isProposedPlan?: boolean;
|
||||||
|
setPlanItems?: Dispatch<SetStateAction<PlanItem[]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PlanViewer({
|
export function PlanViewer({
|
||||||
planItems,
|
planItems,
|
||||||
className,
|
className,
|
||||||
isProposedPlan,
|
isProposedPlan,
|
||||||
|
setPlanItems,
|
||||||
}: PlanViewerProps) {
|
}: PlanViewerProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState<Record<number, boolean>>({});
|
||||||
|
const [newPlanItem, setNewPlanItem] = useState("");
|
||||||
const currentTaskIndex = planItems
|
const currentTaskIndex = planItems
|
||||||
.filter((item) => !item.completed)
|
.filter((item) => !item.completed)
|
||||||
.reduce(
|
.reduce(
|
||||||
|
|
@ -77,37 +85,96 @@ export function PlanViewer({
|
||||||
|
|
||||||
{/* Task Content */}
|
{/* Task Content */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="mb-1 flex items-center gap-2">
|
<div className="mb-1 flex items-center justify-between text-gray-500">
|
||||||
<span className="text-xs font-medium text-gray-500">
|
<div className="flex items-center gap-2">
|
||||||
Step {item.index + 1}
|
<span className="text-xs font-medium">
|
||||||
</span>
|
Step {item.index + 1}
|
||||||
{status === "current" && (
|
|
||||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
|
|
||||||
In Progress
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{status === "completed" && (
|
|
||||||
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
|
||||||
Done
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{status === "proposed" && (
|
|
||||||
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">
|
|
||||||
Proposed
|
|
||||||
</span>
|
</span>
|
||||||
|
{status === "current" && (
|
||||||
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
|
||||||
|
In Progress
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{status === "completed" && (
|
||||||
|
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
||||||
|
Done
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{status === "proposed" && (
|
||||||
|
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">
|
||||||
|
Proposed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isProposedPlan && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TooltipIconButton
|
||||||
|
onClick={() =>
|
||||||
|
setIsEditing((p) => ({
|
||||||
|
...p,
|
||||||
|
[item.index]: !p[item.index],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
tooltip={
|
||||||
|
isEditing[item.index] ? `Save Plan` : `Edit Plan`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isEditing[item.index] ? (
|
||||||
|
<Check className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Pencil className="size-4" />
|
||||||
|
)}
|
||||||
|
</TooltipIconButton>
|
||||||
|
<TooltipIconButton
|
||||||
|
onClick={() =>
|
||||||
|
setPlanItems?.((prev) => {
|
||||||
|
const newArr = prev.filter(
|
||||||
|
(i) => i.index !== item.index,
|
||||||
|
);
|
||||||
|
return newArr.map((i, index) => ({
|
||||||
|
...i,
|
||||||
|
index,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tooltip="Delete Plan"
|
||||||
|
className="transition-colors hover:text-red-500"
|
||||||
|
>
|
||||||
|
<Trash className="size-4" />
|
||||||
|
</TooltipIconButton>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p
|
{isEditing[item.index] ? (
|
||||||
className={cn("text-sm leading-relaxed", {
|
<Textarea
|
||||||
"text-gray-900": status === "current",
|
value={planItems[item.index].plan}
|
||||||
"text-gray-600":
|
onChange={(e) => {
|
||||||
status === "completed" ||
|
setPlanItems?.((prev) => {
|
||||||
["remaining", "proposed"].includes(status),
|
return prev.map((i) => {
|
||||||
})}
|
if (i.index === item.index) {
|
||||||
>
|
return {
|
||||||
{item.plan}
|
...i,
|
||||||
</p>
|
plan: e.target.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={cn("text-sm leading-relaxed", {
|
||||||
|
"text-gray-900": status === "current",
|
||||||
|
"text-gray-600":
|
||||||
|
status === "completed" ||
|
||||||
|
["remaining", "proposed"].includes(status),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{planItems[item.index].plan}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Summary for completed tasks */}
|
{/* Summary for completed tasks */}
|
||||||
{item.summary && status === "completed" && (
|
{item.summary && status === "completed" && (
|
||||||
|
|
@ -122,6 +189,37 @@ export function PlanViewer({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isProposedPlan && (
|
||||||
|
<div
|
||||||
|
key="add-new-plan-item"
|
||||||
|
className="flex items-start gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3 transition-colors"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Add new plan item"
|
||||||
|
value={newPlanItem}
|
||||||
|
onChange={(e) => setNewPlanItem(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPlanItems?.((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
index: prev.length,
|
||||||
|
plan: newPlanItem,
|
||||||
|
completed: false,
|
||||||
|
summary: undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setNewPlanItem("");
|
||||||
|
}}
|
||||||
|
className="mt-auto"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
46
apps/web/src/components/plan/proposed-plan.tsx
Normal file
46
apps/web/src/components/plan/proposed-plan.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { PlanViewer } from "./plan-viewer";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { useProposedPlan } from "../thread/agent-inbox/hooks/useProposedPlan";
|
||||||
|
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
|
export function ProposedPlan({
|
||||||
|
originalPlanItems,
|
||||||
|
}: {
|
||||||
|
originalPlanItems: PlanItem[];
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
planItems,
|
||||||
|
setPlanItems,
|
||||||
|
changesMade,
|
||||||
|
handleResumePlan,
|
||||||
|
handleRejectPlan,
|
||||||
|
} = useProposedPlan(originalPlanItems);
|
||||||
|
|
||||||
|
if (!planItems.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-4 flex flex-col gap-4">
|
||||||
|
<PlanViewer
|
||||||
|
planItems={planItems}
|
||||||
|
setPlanItems={setPlanItems}
|
||||||
|
isProposedPlan={true}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full flex-row items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleResumePlan}
|
||||||
|
className="w-full"
|
||||||
|
variant="brand"
|
||||||
|
>
|
||||||
|
{changesMade ? "Submit" : "Approve"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleRejectPlan}
|
||||||
|
className="w-full"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,6 @@ import { MarkdownText } from "../../markdown-text";
|
||||||
import { ActionRequest, HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
import { ActionRequest, HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { PlanViewer } from "@/components/plan";
|
|
||||||
import { isPlanData, parsePlanData, getPlanKey } from "@/lib/plan-utils";
|
|
||||||
|
|
||||||
interface InboxItemInputProps {
|
interface InboxItemInputProps {
|
||||||
interruptValue: HumanInterrupt;
|
interruptValue: HumanInterrupt;
|
||||||
|
|
@ -50,36 +48,7 @@ function ResetButton({ handleReset }: { handleReset: () => void }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArgsRenderer({
|
function ArgsRenderer({ args }: { args: Record<string, any> }) {
|
||||||
args,
|
|
||||||
actionName,
|
|
||||||
}: {
|
|
||||||
args: Record<string, any>;
|
|
||||||
actionName: string;
|
|
||||||
}) {
|
|
||||||
if (isPlanData(args, actionName)) {
|
|
||||||
const planItems = parsePlanData(args);
|
|
||||||
const planKey = getPlanKey(args);
|
|
||||||
|
|
||||||
if (planItems.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col items-start gap-6">
|
|
||||||
<div className="flex w-full flex-col items-start gap-1">
|
|
||||||
<p className="text-sm leading-[18px] text-wrap text-gray-600">
|
|
||||||
{prettifyText(planKey || "plan")}:
|
|
||||||
</p>
|
|
||||||
<div className="w-full max-w-full rounded-xl border border-gray-200 bg-white p-4">
|
|
||||||
<PlanViewer
|
|
||||||
planItems={planItems}
|
|
||||||
isProposedPlan
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-start gap-6">
|
<div className="flex w-full flex-col items-start gap-6">
|
||||||
{Object.entries(args).map(([k, v]) => {
|
{Object.entries(args).map(([k, v]) => {
|
||||||
|
|
@ -151,10 +120,7 @@ function ResponseComponent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showArgsInResponse && (
|
{showArgsInResponse && (
|
||||||
<ArgsRenderer
|
<ArgsRenderer args={interruptValue.action_request.args} />
|
||||||
args={interruptValue.action_request.args}
|
|
||||||
actionName={interruptValue.action_request.action}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-start gap-[6px]">
|
<div className="flex w-full flex-col items-start gap-[6px]">
|
||||||
|
|
@ -186,12 +152,10 @@ const Response = React.memo(ResponseComponent);
|
||||||
function AcceptComponent({
|
function AcceptComponent({
|
||||||
streaming,
|
streaming,
|
||||||
actionRequestArgs,
|
actionRequestArgs,
|
||||||
actionName,
|
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
}: {
|
}: {
|
||||||
streaming: boolean;
|
streaming: boolean;
|
||||||
actionRequestArgs: Record<string, any>;
|
actionRequestArgs: Record<string, any>;
|
||||||
actionName: string;
|
|
||||||
handleSubmit: (
|
handleSubmit: (
|
||||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent,
|
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | React.KeyboardEvent,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
@ -199,10 +163,7 @@ function AcceptComponent({
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-start gap-4 rounded-lg border-[1px] border-gray-300 p-6">
|
<div className="flex w-full flex-col items-start gap-4 rounded-lg border-[1px] border-gray-300 p-6">
|
||||||
{actionRequestArgs && Object.keys(actionRequestArgs).length > 0 && (
|
{actionRequestArgs && Object.keys(actionRequestArgs).length > 0 && (
|
||||||
<ArgsRenderer
|
<ArgsRenderer args={actionRequestArgs} />
|
||||||
args={actionRequestArgs}
|
|
||||||
actionName={actionName}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="brand"
|
variant="brand"
|
||||||
|
|
@ -238,13 +199,10 @@ function EditAndOrAcceptComponent({
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const defaultRows = React.useRef<Record<string, number>>({});
|
const defaultRows = React.useRef<Record<string, number>>({});
|
||||||
const [editingField, setEditingField] = React.useState<string | null>(null);
|
|
||||||
|
|
||||||
const editResponse = humanResponse.find((r) => r.type === "edit");
|
const editResponse = humanResponse.find((r) => r.type === "edit");
|
||||||
const acceptResponse = humanResponse.find((r) => r.type === "accept");
|
const acceptResponse = humanResponse.find((r) => r.type === "accept");
|
||||||
|
|
||||||
const actionName = interruptValue.action_request.action;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!editResponse ||
|
!editResponse ||
|
||||||
typeof editResponse.args !== "object" ||
|
typeof editResponse.args !== "object" ||
|
||||||
|
|
@ -253,7 +211,6 @@ function EditAndOrAcceptComponent({
|
||||||
if (acceptResponse) {
|
if (acceptResponse) {
|
||||||
return (
|
return (
|
||||||
<AcceptComponent
|
<AcceptComponent
|
||||||
actionName={actionName}
|
|
||||||
actionRequestArgs={interruptValue.action_request.args}
|
actionRequestArgs={interruptValue.action_request.args}
|
||||||
streaming={streaming}
|
streaming={streaming}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
|
@ -315,11 +272,6 @@ function EditAndOrAcceptComponent({
|
||||||
? v
|
? v
|
||||||
: JSON.stringify(v, null);
|
: JSON.stringify(v, null);
|
||||||
|
|
||||||
// Check if this field contains plan data and user isn't actively editing it
|
|
||||||
const isFieldBeingEdited = editingField === k;
|
|
||||||
const fieldArgs = { [k]: v };
|
|
||||||
const isFieldPlanData = isPlanData(fieldArgs, actionName);
|
|
||||||
|
|
||||||
// Calculate the default number of rows by the total length of the initial value divided by 30
|
// Calculate the default number of rows by the total length of the initial value divided by 30
|
||||||
// or 8, whichever is greater. Stored in a ref to prevent re-rendering.
|
// or 8, whichever is greater. Stored in a ref to prevent re-rendering.
|
||||||
if (
|
if (
|
||||||
|
|
@ -339,49 +291,16 @@ function EditAndOrAcceptComponent({
|
||||||
key={`allow-edit-args--${k}-${idx}`}
|
key={`allow-edit-args--${k}-${idx}`}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-col items-start gap-[6px]">
|
<div className="flex w-full flex-col items-start gap-[6px]">
|
||||||
<div className="flex w-full items-center justify-between">
|
<p className="min-w-fit text-sm font-medium">{prettifyText(k)}</p>
|
||||||
<p className="min-w-fit text-sm font-medium">
|
|
||||||
{prettifyText(k)}
|
|
||||||
</p>
|
|
||||||
{isFieldPlanData && !isFieldBeingEdited && (
|
|
||||||
<button
|
|
||||||
onClick={() => setEditingField(k)}
|
|
||||||
className="rounded border border-blue-200 px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 hover:text-blue-800"
|
|
||||||
>
|
|
||||||
Edit as Text
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{isFieldBeingEdited && (
|
|
||||||
<button
|
|
||||||
onClick={() => setEditingField(null)}
|
|
||||||
className="rounded border border-gray-200 px-2 py-1 text-xs text-gray-600 hover:bg-gray-50 hover:text-gray-800"
|
|
||||||
>
|
|
||||||
Show Plan View
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isFieldPlanData && !isFieldBeingEdited ? (
|
<Textarea
|
||||||
// Show plan viewer for plan data when not editing
|
disabled={streaming}
|
||||||
<div className="w-full rounded-lg border border-gray-200 bg-white p-3">
|
className="h-full"
|
||||||
<PlanViewer
|
value={value}
|
||||||
planItems={parsePlanData(fieldArgs)}
|
onChange={(e) => onEditChange(e.target.value, editResponse, k)}
|
||||||
isProposedPlan
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
rows={numRows}
|
||||||
</div>
|
/>
|
||||||
) : (
|
|
||||||
// Show textarea for editing or non-plan data
|
|
||||||
<Textarea
|
|
||||||
disabled={streaming}
|
|
||||||
className="h-full"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) =>
|
|
||||||
onEditChange(e.target.value, editResponse, k)
|
|
||||||
}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
rows={numRows}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -424,7 +343,6 @@ export function InboxItemInput({
|
||||||
hasArgs && !isEditAllowed && !acceptAllowed && isResponseAllowed;
|
hasArgs && !isEditAllowed && !acceptAllowed && isResponseAllowed;
|
||||||
const showArgsOutsideActionCards =
|
const showArgsOutsideActionCards =
|
||||||
hasArgs && !showArgsInResponse && !isEditAllowed && !acceptAllowed;
|
hasArgs && !showArgsInResponse && !isEditAllowed && !acceptAllowed;
|
||||||
const actionName = interruptValue.action_request.action;
|
|
||||||
|
|
||||||
const onEditChange = (
|
const onEditChange = (
|
||||||
change: string | string[],
|
change: string | string[],
|
||||||
|
|
@ -580,10 +498,7 @@ export function InboxItemInput({
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col items-start justify-start gap-2">
|
<div className="flex w-full flex-col items-start justify-start gap-2">
|
||||||
{showArgsOutsideActionCards && (
|
{showArgsOutsideActionCards && (
|
||||||
<ArgsRenderer
|
<ArgsRenderer args={interruptValue.action_request.args} />
|
||||||
args={interruptValue.action_request.args}
|
|
||||||
actionName={actionName}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-col items-start gap-2">
|
<div className="flex w-full flex-col items-start gap-2">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { PlanItem } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { convertPlanItemsToInterruptString } from "@/lib/plan-utils";
|
||||||
|
import { useStreamContext } from "@/providers/Stream";
|
||||||
|
import { PLAN_INTERRUPT_ACTION_TITLE } from "@open-swe/shared/constants";
|
||||||
|
|
||||||
|
export function useProposedPlan(originalPlanItems: PlanItem[]) {
|
||||||
|
const stream = useStreamContext();
|
||||||
|
const [planItems, setPlanItems] = useState<PlanItem[]>(originalPlanItems);
|
||||||
|
const [changesMade, setChangesMade] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setChangesMade(
|
||||||
|
JSON.stringify(originalPlanItems) !== JSON.stringify(planItems),
|
||||||
|
);
|
||||||
|
}, [originalPlanItems, planItems]);
|
||||||
|
|
||||||
|
const handleResumePlan = () => {
|
||||||
|
let resume: HumanResponse[];
|
||||||
|
if (changesMade) {
|
||||||
|
resume = [
|
||||||
|
{
|
||||||
|
type: "edit",
|
||||||
|
args: {
|
||||||
|
action: PLAN_INTERRUPT_ACTION_TITLE,
|
||||||
|
args: {
|
||||||
|
plan: convertPlanItemsToInterruptString(planItems),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
resume = [
|
||||||
|
{
|
||||||
|
type: "accept",
|
||||||
|
args: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
stream.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
command: {
|
||||||
|
resume,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
recursion_limit: 400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRejectPlan = () => {
|
||||||
|
const resume: HumanResponse[] = [
|
||||||
|
{
|
||||||
|
type: "ignore",
|
||||||
|
args: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
stream.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
command: {
|
||||||
|
resume,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
recursion_limit: 400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
changesMade,
|
||||||
|
planItems,
|
||||||
|
setPlanItems,
|
||||||
|
handleResumePlan,
|
||||||
|
handleRejectPlan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ import { ThreadActionsView } from "./components/thread-actions-view";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
import { HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
||||||
import { useStreamContext } from "@/providers/Stream";
|
import { useStreamContext } from "@/providers/Stream";
|
||||||
|
import { parsePlanData } from "@/lib/plan-utils";
|
||||||
|
import { ProposedPlan } from "@/components/plan/proposed-plan";
|
||||||
|
|
||||||
interface ThreadViewProps {
|
interface ThreadViewProps {
|
||||||
interrupt: HumanInterrupt | HumanInterrupt[];
|
interrupt: HumanInterrupt | HumanInterrupt[];
|
||||||
|
|
@ -35,6 +37,11 @@ export function ThreadView({ interrupt }: ThreadViewProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const planItems = parsePlanData(interruptObj.action_request.args);
|
||||||
|
if (planItems?.length) {
|
||||||
|
return <ProposedPlan originalPlanItems={planItems} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[80vh] w-full flex-col overflow-y-scroll rounded-2xl bg-gray-50/50 p-8 lg:flex-row [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
|
<div className="flex h-[80vh] w-full flex-col overflow-y-scroll rounded-2xl bg-gray-50/50 p-8 lg:flex-row [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
|
||||||
{showSidePanel ? (
|
{showSidePanel ? (
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ import {
|
||||||
import { BaseMessage } from "@langchain/core/messages";
|
import { BaseMessage } from "@langchain/core/messages";
|
||||||
import { TaskPlanView } from "../tasks";
|
import { TaskPlanView } from "../tasks";
|
||||||
import { useTaskPlan } from "../tasks/useTaskPlan";
|
import { useTaskPlan } from "../tasks/useTaskPlan";
|
||||||
|
import { isProposedPlanInterrupt } from "@/lib/plan-utils";
|
||||||
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
|
|
||||||
function StickyToBottomContent(props: {
|
function StickyToBottomContent(props: {
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
|
|
@ -111,8 +113,6 @@ export function Thread() {
|
||||||
"chatHistoryOpen",
|
"chatHistoryOpen",
|
||||||
parseAsBoolean.withDefault(false),
|
parseAsBoolean.withDefault(false),
|
||||||
);
|
);
|
||||||
const [baseBranch, setBaseBranch] = useQueryState("base-branch");
|
|
||||||
|
|
||||||
const [configSidebarOpen, setConfigSidebarOpen] = useState(false);
|
const [configSidebarOpen, setConfigSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const isTaskView = !!taskId;
|
const isTaskView = !!taskId;
|
||||||
|
|
@ -227,6 +227,35 @@ export function Thread() {
|
||||||
|
|
||||||
setFirstTokenReceived(false);
|
setFirstTokenReceived(false);
|
||||||
|
|
||||||
|
if (isProposedPlanInterrupt(stream.interrupt)) {
|
||||||
|
const resume: HumanResponse[] = [
|
||||||
|
{
|
||||||
|
type: "response",
|
||||||
|
args: input.trim(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
stream.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
command: {
|
||||||
|
resume,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (contentBlocks.length > 0) {
|
||||||
|
toast.warning(
|
||||||
|
"Content blocks were not submitted with the plan response.",
|
||||||
|
{
|
||||||
|
richColors: true,
|
||||||
|
closeButton: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setInput("");
|
||||||
|
setContentBlocks([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newHumanMessage: Message = {
|
const newHumanMessage: Message = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
type: "human",
|
type: "human",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import {
|
||||||
PLAN_INTERRUPT_ACTION_TITLE,
|
PLAN_INTERRUPT_ACTION_TITLE,
|
||||||
PLAN_INTERRUPT_DELIMITER,
|
PLAN_INTERRUPT_DELIMITER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
|
import { Interrupt } from "@langchain/langgraph-sdk";
|
||||||
|
import { HumanInterrupt } from "@langchain/langgraph/prebuilt";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the interrupt args contain plan data
|
* Checks if the interrupt args contain plan data
|
||||||
|
|
@ -30,7 +32,7 @@ export function parsePlanData(args: Record<string, any>): PlanItem[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
plan: args.plan,
|
plan: args.plan.trim(),
|
||||||
completed: false,
|
completed: false,
|
||||||
summary: undefined,
|
summary: undefined,
|
||||||
},
|
},
|
||||||
|
|
@ -40,7 +42,7 @@ export function parsePlanData(args: Record<string, any>): PlanItem[] {
|
||||||
.split(PLAN_INTERRUPT_DELIMITER)
|
.split(PLAN_INTERRUPT_DELIMITER)
|
||||||
.map((item: string, index: number) => ({
|
.map((item: string, index: number) => ({
|
||||||
index,
|
index,
|
||||||
plan: item,
|
plan: item.trim(),
|
||||||
completed: false,
|
completed: false,
|
||||||
summary: undefined,
|
summary: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
@ -54,3 +56,23 @@ export function getPlanKey(args: Record<string, any>): string | null {
|
||||||
if (args.plan && typeof args.plan === "string") return "plan";
|
if (args.plan && typeof args.plan === "string") return "plan";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PlanItem array to interrupt string
|
||||||
|
*/
|
||||||
|
export function convertPlanItemsToInterruptString(
|
||||||
|
planItems: PlanItem[],
|
||||||
|
): string {
|
||||||
|
return planItems.map((item) => item.plan).join(PLAN_INTERRUPT_DELIMITER);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProposedPlanInterrupt(
|
||||||
|
interrupt: Interrupt | undefined,
|
||||||
|
): boolean {
|
||||||
|
return interrupt?.value && typeof interrupt?.value === "object"
|
||||||
|
? isPlanData(
|
||||||
|
(interrupt.value as HumanInterrupt)?.action_request?.args ?? {},
|
||||||
|
(interrupt.value as HumanInterrupt)?.action_request?.action ?? "",
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@langchain/langgraph-sdk": "^0.0.83"
|
"@langchain/langgraph-sdk": "^0.0.83",
|
||||||
|
"@langchain/core": "^0.3.58"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@3.5.1"
|
"packageManager": "yarn@3.5.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/core": "^0.3.56",
|
"@langchain/core": "^0.3.56",
|
||||||
"@langchain/langgraph": "^0.3.1",
|
"@langchain/langgraph": "^0.3.3",
|
||||||
"@langchain/langgraph-sdk": "^0.0.83",
|
"@langchain/langgraph-sdk": "^0.0.83",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.25.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
|
|
||||||
|
|
@ -125,19 +125,16 @@ export const GraphAnnotation = MessagesZodState.extend({
|
||||||
* passed to the LLM, truncated, removed etc. The main `messages`
|
* passed to the LLM, truncated, removed etc. The main `messages`
|
||||||
* key is never modified to persist the content show on the client.
|
* key is never modified to persist the content show on the client.
|
||||||
*/
|
*/
|
||||||
internalMessages: withLangGraph<BaseMessage[], Messages>(
|
internalMessages: withLangGraph(z.custom<BaseMessage[]>(), {
|
||||||
z.custom<BaseMessage[]>(),
|
reducer: {
|
||||||
{
|
schema: z.custom<Messages>(),
|
||||||
reducer: {
|
fn: messagesStateReducer,
|
||||||
schema: z.custom<Messages>(),
|
|
||||||
fn: messagesStateReducer,
|
|
||||||
},
|
|
||||||
jsonSchemaExtra: {
|
|
||||||
langgraph_type: "messages",
|
|
||||||
},
|
|
||||||
default: () => [],
|
|
||||||
},
|
},
|
||||||
),
|
jsonSchemaExtra: {
|
||||||
|
langgraph_type: "messages",
|
||||||
|
},
|
||||||
|
default: () => [],
|
||||||
|
}),
|
||||||
proposedPlan: z
|
proposedPlan: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.default(() => [])
|
.default(() => [])
|
||||||
|
|
|
||||||
42
yarn.lock
42
yarn.lock
|
|
@ -1694,9 +1694,9 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@langchain/core@npm:^0.3.56, @langchain/core@npm:^0.3.57":
|
"@langchain/core@npm:^0.3.58":
|
||||||
version: 0.3.57
|
version: 0.3.58
|
||||||
resolution: "@langchain/core@npm:0.3.57"
|
resolution: "@langchain/core@npm:0.3.58"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cfworker/json-schema": ^4.0.2
|
"@cfworker/json-schema": ^4.0.2
|
||||||
ansi-styles: ^5.0.0
|
ansi-styles: ^5.0.0
|
||||||
|
|
@ -1708,9 +1708,9 @@ __metadata:
|
||||||
p-queue: ^6.6.2
|
p-queue: ^6.6.2
|
||||||
p-retry: 4
|
p-retry: 4
|
||||||
uuid: ^10.0.0
|
uuid: ^10.0.0
|
||||||
zod: ^3.22.4
|
zod: ^3.25.32
|
||||||
zod-to-json-schema: ^3.22.3
|
zod-to-json-schema: ^3.22.3
|
||||||
checksum: 5a497553209a3fe0c2a31b23c9586f586ea8b0785cb437a0b7140c308be80f0caa4397c45a4d7f0a1b7056f3509b03663f6c714217b75c20cce0a2df4fb3c105
|
checksum: 0dece4accb0da6c67ac9286614b846dafc8e53c39435e404781bb436562ac8f55c474b099b182b44b3b4fdab16f8a9020add5ca804a6d9811b23ec940b7f3589
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -1838,21 +1838,21 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@langchain/langgraph@npm:^0.3.1":
|
"@langchain/langgraph@npm:^0.3.3":
|
||||||
version: 0.3.1
|
version: 0.3.3
|
||||||
resolution: "@langchain/langgraph@npm:0.3.1"
|
resolution: "@langchain/langgraph@npm:0.3.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@langchain/langgraph-checkpoint": ~0.0.18
|
"@langchain/langgraph-checkpoint": ~0.0.18
|
||||||
"@langchain/langgraph-sdk": ~0.0.32
|
"@langchain/langgraph-sdk": ~0.0.32
|
||||||
uuid: ^10.0.0
|
uuid: ^10.0.0
|
||||||
zod: ^3.23.8
|
zod: ^3.25.32
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0"
|
"@langchain/core": ">=0.3.58 < 0.4.0"
|
||||||
zod-to-json-schema: ^3.x
|
zod-to-json-schema: ^3.x
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
zod-to-json-schema:
|
zod-to-json-schema:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 25f4a338635c00a0bb56be483438eef29a133b3900cec91b0a32e880ba0b6ee719ea0365f957cc7a5c25218f50f10b83975f2198565aafc27d17127fabf188cc
|
checksum: 26a9fc9003f0c57f892f7d45f1d99ab1b028bdbe73a32a2f5cbc1786da854bc875c6008c157e314aa338af376c9ae546f136dac8d1b8e3807040f3d3c6fa3c73
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
@ -2246,7 +2246,7 @@ __metadata:
|
||||||
"@langchain/anthropic": ^0.3.20
|
"@langchain/anthropic": ^0.3.20
|
||||||
"@langchain/core": ^0.3.56
|
"@langchain/core": ^0.3.56
|
||||||
"@langchain/google-genai": ^0.2.9
|
"@langchain/google-genai": ^0.2.9
|
||||||
"@langchain/langgraph": ^0.3.1
|
"@langchain/langgraph": ^0.3.3
|
||||||
"@langchain/langgraph-cli": latest
|
"@langchain/langgraph-cli": latest
|
||||||
"@langchain/langgraph-sdk": ^0.0.83
|
"@langchain/langgraph-sdk": ^0.0.83
|
||||||
"@langchain/openai": ^0.5.10
|
"@langchain/openai": ^0.5.10
|
||||||
|
|
@ -2271,7 +2271,7 @@ __metadata:
|
||||||
typescript: ~5.7.2
|
typescript: ~5.7.2
|
||||||
typescript-eslint: ^8.22.0
|
typescript-eslint: ^8.22.0
|
||||||
uuid: ^11.0.5
|
uuid: ^11.0.5
|
||||||
zod: ^3.23.8
|
zod: ^3.25.32
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
@ -2282,7 +2282,7 @@ __metadata:
|
||||||
"@eslint/eslintrc": ^3.1.0
|
"@eslint/eslintrc": ^3.1.0
|
||||||
"@eslint/js": ^9.19.0
|
"@eslint/js": ^9.19.0
|
||||||
"@langchain/core": ^0.3.56
|
"@langchain/core": ^0.3.56
|
||||||
"@langchain/langgraph": ^0.3.1
|
"@langchain/langgraph": ^0.3.3
|
||||||
"@langchain/langgraph-sdk": ^0.0.83
|
"@langchain/langgraph-sdk": ^0.0.83
|
||||||
"@tsconfig/recommended": ^1.0.8
|
"@tsconfig/recommended": ^1.0.8
|
||||||
"@types/node": ^22.13.5
|
"@types/node": ^22.13.5
|
||||||
|
|
@ -2295,7 +2295,7 @@ __metadata:
|
||||||
prettier: ^3.5.2
|
prettier: ^3.5.2
|
||||||
typescript: ~5.7.2
|
typescript: ~5.7.2
|
||||||
typescript-eslint: ^8.22.0
|
typescript-eslint: ^8.22.0
|
||||||
zod: ^3.23.8
|
zod: ^3.25.32
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
@ -2305,7 +2305,7 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/js": ^9.19.0
|
"@eslint/js": ^9.19.0
|
||||||
"@langchain/core": ^0.3.57
|
"@langchain/core": ^0.3.57
|
||||||
"@langchain/langgraph": ^0.3.1
|
"@langchain/langgraph": ^0.3.3
|
||||||
"@langchain/langgraph-sdk": ^0.0.83
|
"@langchain/langgraph-sdk": ^0.0.83
|
||||||
"@open-swe/shared": "*"
|
"@open-swe/shared": "*"
|
||||||
"@radix-ui/react-alert-dialog": ^1.1.14
|
"@radix-ui/react-alert-dialog": ^1.1.14
|
||||||
|
|
@ -2374,7 +2374,7 @@ __metadata:
|
||||||
typescript-eslint: ^8.22.0
|
typescript-eslint: ^8.22.0
|
||||||
use-stick-to-bottom: ^1.0.46
|
use-stick-to-bottom: ^1.0.46
|
||||||
uuid: ^11.1.0
|
uuid: ^11.1.0
|
||||||
zod: ^3.24.2
|
zod: ^3.25.32
|
||||||
zustand: ^5.0.5
|
zustand: ^5.0.5
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
@ -14526,10 +14526,10 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"zod@npm:^3.24.2":
|
"zod@npm:^3.25.32":
|
||||||
version: 3.25.32
|
version: 3.25.64
|
||||||
resolution: "zod@npm:3.25.32"
|
resolution: "zod@npm:3.25.64"
|
||||||
checksum: 27528887ebb778c70ec753e19c8cde453d36cd2bf4682e2f19a441cd9418f87ea85f6da550ab9b157a625b4d19b9c65921e11bf88a76702051528ad4c5bcf2f0
|
checksum: 9f0e86430ed52ef56930712485573454f9b8d64697e24e3139581489520993658d2dee5e939e9e820c1c720afa202fdca58a3ef096f8d91b0d04eb4285bc00ca
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue