Merge pull request #32 from langchain-ai/brace/open-code

feat: Open Code agent
This commit is contained in:
Brace Sproul
2025-03-07 14:24:29 -08:00
committed by GitHub
22 changed files with 840 additions and 370 deletions

View File

@@ -6,9 +6,11 @@ import { stockbrokerGraph } from "./stockbroker";
import { ChatOpenAI } from "@langchain/openai";
import { tripPlannerGraph } from "./trip-planner";
import { formatMessages } from "./utils/format-messages";
import { graph as openCodeGraph } from "./open-code";
const allToolDescriptions = `- stockbroker: can fetch the price of a ticker, purchase/sell a ticker, or get the user's portfolio
- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location.`;
- tripPlanner: helps the user plan their trip. it can suggest restaurants, and places to stay in any given location.
- openCode: can write code for the user. call this tool when the user asks you to write code`;
async function router(
state: GenerativeUIState,
@@ -19,7 +21,7 @@ ${allToolDescriptions}
`;
const routerSchema = z.object({
route: z
.enum(["stockbroker", "tripPlanner", "generalInput"])
.enum(["stockbroker", "tripPlanner", "openCode", "generalInput"])
.describe(routerDescription),
});
const routerTool = {
@@ -73,7 +75,7 @@ Please pick the proper route based on the most recent message, in the context of
function handleRoute(
state: GenerativeUIState,
): "stockbroker" | "tripPlanner" | "generalInput" {
): "stockbroker" | "tripPlanner" | "openCode" | "generalInput" {
return state.next;
}
@@ -104,16 +106,19 @@ const builder = new StateGraph(GenerativeUIAnnotation)
.addNode("router", router)
.addNode("stockbroker", stockbrokerGraph)
.addNode("tripPlanner", tripPlannerGraph)
.addNode("openCode", openCodeGraph)
.addNode("generalInput", handleGeneralInput)
.addConditionalEdges("router", handleRoute, [
"stockbroker",
"tripPlanner",
"openCode",
"generalInput",
])
.addEdge(START, "router")
.addEdge("stockbroker", END)
.addEdge("tripPlanner", END)
.addEdge("openCode", END)
.addEdge("generalInput", END);
export const graph = builder.compile();

45
agent/open-code/index.ts Normal file
View File

@@ -0,0 +1,45 @@
import {
END,
LangGraphRunnableConfig,
START,
StateGraph,
} from "@langchain/langgraph";
import { OpenCodeAnnotation, OpenCodeState } from "./types";
import { planner } from "./nodes/planner";
import {
executor,
SUCCESSFULLY_COMPLETED_STEPS_CONTENT,
} from "./nodes/executor";
import { AIMessage } from "@langchain/langgraph-sdk";
function conditionallyEnd(
state: OpenCodeState,
config: LangGraphRunnableConfig,
): typeof END | "planner" {
const fullWriteAccess = !!config.configurable?.permissions?.full_write_access;
const lastAiMessage = state.messages.findLast(
(m) => m.getType() === "ai",
) as unknown as AIMessage;
// If the user did not grant full write access, or the last AI message is the success message, end
// otherwise, loop back to the start.
if (
(typeof lastAiMessage.content === "string" &&
lastAiMessage.content === SUCCESSFULLY_COMPLETED_STEPS_CONTENT) ||
!fullWriteAccess
) {
return END;
}
return "planner";
}
const workflow = new StateGraph(OpenCodeAnnotation)
.addNode("planner", planner)
.addNode("executor", executor)
.addEdge(START, "planner")
.addEdge("planner", "executor")
.addConditionalEdges("executor", conditionallyEnd, ["planner", END]);
export const graph = workflow.compile();
graph.name = "Open Code Graph";

View File

@@ -0,0 +1,122 @@
import fs from "fs/promises";
import { v4 as uuidv4 } from "uuid";
import { AIMessage } from "@langchain/langgraph-sdk";
import { OpenCodeState, OpenCodeUpdate } from "../types";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import ComponentMap from "../../uis";
import { typedUi } from "@langchain/langgraph-sdk/react-ui/server";
export const SUCCESSFULLY_COMPLETED_STEPS_CONTENT =
"Successfully completed all the steps in the plan. Please let me know if you need anything else!";
export async function executor(
state: OpenCodeState,
config: LangGraphRunnableConfig,
): Promise<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(config);
const lastPlanToolCall = state.messages.findLast(
(m) =>
m.getType() === "ai" &&
(m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"),
) as AIMessage | undefined;
const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args;
const nextPlanItem = planToolCallArgs?.remainingPlans?.[0] as
| string
| undefined;
const numSeenPlans =
[
...(planToolCallArgs?.executedPlans ?? []),
...(planToolCallArgs?.rejectedPlans ?? []),
]?.length ?? 0;
if (!nextPlanItem) {
// All plans have been executed
const successfullyFinishedMsg: AIMessage = {
type: "ai",
id: uuidv4(),
content: SUCCESSFULLY_COMPLETED_STEPS_CONTENT,
};
return { messages: [successfullyFinishedMsg] };
}
let updateFileContents = "";
switch (numSeenPlans) {
case 0:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-1.txt",
"utf-8",
);
break;
case 1:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-2.txt",
"utf-8",
);
break;
case 2:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-3.txt",
"utf-8",
);
break;
case 3:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-4.txt",
"utf-8",
);
break;
case 4:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-5.txt",
"utf-8",
);
break;
case 5:
updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-6.txt",
"utf-8",
);
break;
default:
updateFileContents = "";
}
if (!updateFileContents) {
throw new Error("No file updates found!");
}
const toolCallId = uuidv4();
const aiMessage: AIMessage = {
type: "ai",
id: uuidv4(),
content: "",
tool_calls: [
{
name: "update_file",
args: {
new_file_content: updateFileContents,
executed_plan_item: nextPlanItem,
},
id: toolCallId,
type: "tool_call",
},
],
};
const fullWriteAccess = !!config.configurable?.permissions?.full_write_access;
const msg = ui.create("proposed-change", {
toolCallId,
change: updateFileContents,
planItem: nextPlanItem,
fullWriteAccess,
});
msg.additional_kwargs["message_id"] = aiMessage.id;
return {
messages: [aiMessage],
ui: [msg],
timestamp: Date.now(),
};
}

View File

@@ -0,0 +1,5 @@
```bash
npx create-react-app todo-app --template typescript
cd todo-app
mkdir -p src/{components,styles,utils}
```

View File

@@ -0,0 +1,21 @@
```tsx
// src/components/TodoItem.tsx
import React from 'react';
import styles from '../styles/TodoItem.module.css';
interface TodoItemProps {
id: string;
text: string;
completed: boolean;
onToggle: (id: string) => void;
onDelete: (id: string) => void;
}
export const TodoItem: React.FC<TodoItemProps> = ({ id, text, completed, onToggle, onDelete }) => (
<div className={styles.todoItem}>
<input type='checkbox' checked={completed} onChange={() => onToggle(id)} />
<span className={completed ? styles.completed : ''}>{text}</span>
<button onClick={() => onDelete(id)}>Delete</button>
</div>
);
```

View File

@@ -0,0 +1,22 @@
```tsx
// src/context/TodoContext.tsx
import React, { createContext, useContext, useReducer } from 'react';
type Todo = { id: string; text: string; completed: boolean; };
type TodoState = { todos: Todo[]; };
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: string };
const TodoContext = createContext<{
state: TodoState;
dispatch: React.Dispatch<TodoAction>;
} | undefined>(undefined);
export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, { todos: [] });
return <TodoContext.Provider value={{ state, dispatch }}>{children}</TodoContext.Provider>;
};
```

View File

@@ -0,0 +1,33 @@
```tsx
// src/components/AddTodo.tsx
import React, { useState } from 'react';
import styles from '../styles/AddTodo.module.css';
export const AddTodo: React.FC<{ onAdd: (text: string) => void }> = ({ onAdd }) => {
const [text, setText] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!text.trim()) {
setError('Todo text cannot be empty');
return;
}
onAdd(text.trim());
setText('');
setError('');
};
return (
<form onSubmit={handleSubmit} className={styles.form}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder='Add a new todo'
/>
{error && <div className={styles.error}>{error}</div>}
<button type='submit'>Add Todo</button>
</form>
);
};
```

View File

@@ -0,0 +1,22 @@
```tsx
// src/components/TodoFilters.tsx
import React from 'react';
type FilterType = 'all' | 'active' | 'completed';
export const TodoFilters: React.FC<{
currentFilter: FilterType;
onFilterChange: (filter: FilterType) => void;
onSortChange: (ascending: boolean) => void;
}> = ({ currentFilter, onFilterChange, onSortChange }) => (
<div>
<select value={currentFilter} onChange={(e) => onFilterChange(e.target.value as FilterType)}>
<option value='all'>All</option>
<option value='active'>Active</option>
<option value='completed'>Completed</option>
</select>
<button onClick={() => onSortChange(true)}>Sort A-Z</button>
<button onClick={() => onSortChange(false)}>Sort Z-A</button>
</div>
);
```

View File

@@ -0,0 +1,13 @@
```tsx
// src/utils/storage.ts
const STORAGE_KEY = 'todos';
export const saveTodos = (todos: Todo[]) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
};
export const loadTodos = (): Todo[] => {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
};
```

View File

@@ -0,0 +1,109 @@
import { v4 as uuidv4 } from "uuid";
import { AIMessage, ToolMessage } from "@langchain/langgraph-sdk";
import { OpenCodeState, OpenCodeUpdate } from "../types";
import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses";
import { LangGraphRunnableConfig } from "@langchain/langgraph";
import ComponentMap from "../../uis";
import { typedUi } from "@langchain/langgraph-sdk/react-ui/server";
const PLAN = [
"Set up project scaffolding using Create React App and implement basic folder structure for components, styles, and utilities.",
"Create reusable UI components for TodoItem, including styling with CSS modules.",
"Implement state management using React Context to handle todo items, including actions for adding, updating, and deleting todos.",
"Add form functionality for creating new todos with input validation and error handling.",
"Create filtering and sorting capabilities to allow users to view completed, active, or all todos.",
"Implement local storage integration to persist todo items between page refreshes.",
];
export async function planner(
state: OpenCodeState,
config: LangGraphRunnableConfig,
): Promise<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(config);
const lastUpdateCodeToolCall = state.messages.findLast(
(m) =>
m.getType() === "ai" &&
(m as unknown as AIMessage).tool_calls?.some(
(tc) => tc.name === "update_file",
),
) as AIMessage | undefined;
const lastUpdateToolCallResponse = state.messages.findLast(
(m) =>
m.getType() === "tool" &&
(m as unknown as ToolMessage).tool_call_id ===
lastUpdateCodeToolCall?.tool_calls?.[0]?.id,
) as ToolMessage | undefined;
const lastPlanToolCall = state.messages.findLast(
(m) =>
m.getType() === "ai" &&
(m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"),
) as AIMessage | undefined;
const wasPlanRejected = (
lastUpdateToolCallResponse?.content as string | undefined
)
?.toLowerCase()
.includes("rejected");
const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args;
const executedPlans: string[] = planToolCallArgs?.executedPlans ?? [];
const rejectedPlans: string[] = planToolCallArgs?.rejectedPlans ?? [];
let remainingPlans: string[] = planToolCallArgs?.remainingPlans ?? PLAN;
const proposedChangePlanItem: string | undefined =
lastUpdateCodeToolCall?.tool_calls?.[0]?.args?.executed_plan_item;
if (proposedChangePlanItem) {
if (wasPlanRejected) {
rejectedPlans.push(proposedChangePlanItem);
} else {
executedPlans.push(proposedChangePlanItem);
}
remainingPlans = remainingPlans.filter((p) => p !== proposedChangePlanItem);
}
const content = proposedChangePlanItem
? `I've updated the plan list based on the last proposed change.`
: `I've come up with a detailed plan for building the todo app.`;
const toolCallId = uuidv4();
const aiMessage: AIMessage = {
type: "ai",
id: uuidv4(),
content,
tool_calls: [
{
name: "plan",
args: {
executedPlans,
rejectedPlans,
remainingPlans,
},
id: toolCallId,
type: "tool_call",
},
],
};
const msg = ui.create("code-plan", {
toolCallId,
executedPlans,
rejectedPlans,
remainingPlans,
});
msg.additional_kwargs["message_id"] = aiMessage.id;
const toolMessage: ToolMessage = {
type: "tool",
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
tool_call_id: toolCallId,
content: "User has approved the plan.",
};
return {
messages: [aiMessage, toolMessage],
ui: [msg],
timestamp: Date.now(),
};
}

11
agent/open-code/types.ts Normal file
View File

@@ -0,0 +1,11 @@
import { Annotation } from "@langchain/langgraph";
import { GenerativeUIAnnotation } from "../types";
export const OpenCodeAnnotation = Annotation.Root({
messages: GenerativeUIAnnotation.spec.messages,
ui: GenerativeUIAnnotation.spec.ui,
timestamp: GenerativeUIAnnotation.spec.timestamp,
});
export type OpenCodeState = typeof OpenCodeAnnotation.State;
export type OpenCodeUpdate = typeof OpenCodeAnnotation.Update;

View File

@@ -12,7 +12,9 @@ export const GenerativeUIAnnotation = Annotation.Root({
UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[]
>({ default: () => [], reducer: uiMessageReducer }),
timestamp: Annotation<number>,
next: Annotation<"stockbroker" | "tripPlanner" | "generalInput">(),
next: Annotation<
"stockbroker" | "tripPlanner" | "openCode" | "generalInput"
>(),
});
export type GenerativeUIState = typeof GenerativeUIAnnotation.State;

View File

@@ -5,6 +5,8 @@ import BookAccommodation from "./trip-planner/book-accommodation";
import RestaurantsList from "./trip-planner/restaurants-list";
import BookRestaurant from "./trip-planner/book-restaurant";
import BuyStock from "./stockbroker/buy-stock";
import Plan from "./open-code/plan";
import ProposedChange from "./open-code/proposed-change";
const ComponentMap = {
"stock-price": StockPrice,
@@ -14,5 +16,7 @@ const ComponentMap = {
"restaurants-list": RestaurantsList,
"book-restaurant": BookRestaurant,
"buy-stock": BuyStock,
"code-plan": Plan,
"proposed-change": ProposedChange,
} as const;
export default ComponentMap;

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,76 @@
import "./index.css";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
interface PlanProps {
toolCallId: string;
executedPlans: string[];
rejectedPlans: string[];
remainingPlans: string[];
}
export default function Plan(props: PlanProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="flex flex-col w-full max-w-4xl border-[1px] rounded-xl border-slate-200 overflow-hidden">
<div className="p-6">
<h2 className="text-2xl font-semibold text-left">Code Plan</h2>
</div>
<motion.div
className="relative overflow-hidden"
animate={{
height: isExpanded ? "auto" : "200px",
opacity: isExpanded ? 1 : 0.7,
}}
transition={{
height: { duration: 0.3, ease: [0.4, 0, 0.2, 1] },
opacity: { duration: 0.2 },
}}
initial={false}
>
<div className="grid grid-cols-3 divide-x divide-slate-300 w-full border-t border-slate-200 px-6 pt-4 pb-4">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Remaining Plans
</h3>
{props.remainingPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{index + 1}. {step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Executed Plans
</h3>
{props.executedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
<div className="flex flex-col gap-2 px-6">
<h3 className="text-lg font-medium mb-4 text-slate-700">
Rejected Plans
</h3>
{props.rejectedPlans.map((step, index) => (
<p key={index} className="font-mono text-sm">
{step}
</p>
))}
</div>
</div>
</motion.div>
<motion.button
className="w-full py-2 border-t border-slate-200 flex items-center justify-center hover:bg-slate-50 transition-colors"
onClick={() => setIsExpanded(!isExpanded)}
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
>
<ChevronDown className="w-5 h-5 text-slate-600" />
</motion.button>
</div>
);
}

View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,189 @@
import "./index.css";
import { v4 as uuidv4 } from "uuid";
import { Button } from "@/components/ui/button";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { UIMessage, useStreamContext } from "@langchain/langgraph-sdk/react-ui";
import { Message } from "@langchain/langgraph-sdk";
import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/ensure-tool-responses";
import { useEffect, useState } from "react";
import { getToolResponse } from "../../utils/get-tool-response";
import { cn } from "@/lib/utils";
interface ProposedChangeProps {
toolCallId: string;
change: string;
planItem: string;
/**
* Whether or not to show the "Accept"/"Reject" buttons
* If true, this means the user selected the "Accept, don't ask again"
* button for this session.
*/
fullWriteAccess: boolean;
}
const ACCEPTED_CHANGE_CONTENT =
"User accepted the proposed change. Please continue.";
const REJECTED_CHANGE_CONTENT =
"User rejected the proposed change. Please continue.";
export default function ProposedChange(props: ProposedChangeProps) {
const [isAccepted, setIsAccepted] = useState(false);
const [isRejected, setIsRejected] = useState(false);
const thread = useStreamContext<
{ messages: Message[]; ui: UIMessage[] },
{ MetaType: { ui: UIMessage | undefined } }
>();
const handleReject = () => {
thread.submit({
messages: [
{
type: "tool",
tool_call_id: props.toolCallId,
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
name: "update_file",
content: REJECTED_CHANGE_CONTENT,
},
{
type: "human",
content: `Rejected change.`,
},
],
});
setIsRejected(true);
};
const handleAccept = (shouldGrantFullWriteAccess = false) => {
const humanMessageContent = `Accepted change. ${shouldGrantFullWriteAccess ? "Granted full write access." : ""}`;
thread.submit(
{
messages: [
{
type: "tool",
tool_call_id: props.toolCallId,
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
name: "update_file",
content: ACCEPTED_CHANGE_CONTENT,
},
{
type: "human",
content: humanMessageContent,
},
],
},
{
config: {
configurable: {
permissions: {
full_write_access: shouldGrantFullWriteAccess,
},
},
},
},
);
setIsAccepted(true);
};
useEffect(() => {
if (typeof window === "undefined" || isAccepted) return;
const toolResponse = getToolResponse(props.toolCallId, thread);
if (toolResponse) {
if (toolResponse.content === ACCEPTED_CHANGE_CONTENT) {
setIsAccepted(true);
} else if (toolResponse.content === REJECTED_CHANGE_CONTENT) {
setIsRejected(true);
}
}
}, []);
if (isAccepted || isRejected) {
return (
<div
className={cn(
"flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl",
isAccepted ? "border-green-300" : "border-red-300",
)}
>
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">
{isAccepted ? "Accepted" : "Rejected"} Change
</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
</div>
);
}
return (
<div className="flex flex-col gap-4 w-full max-w-4xl p-4 border-[1px] rounded-xl border-slate-200">
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">Proposed Change</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown
children={props.change}
components={{
code(props) {
const { children, className, node: _node } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={coldarkDark}
/>
) : (
<code className={className}>{children}</code>
);
},
}}
/>
{!props.fullWriteAccess && (
<div className="flex gap-2 items-center w-full">
<Button
className="cursor-pointer w-full"
variant="destructive"
onClick={handleReject}
>
Reject
</Button>
<Button
className="cursor-pointer w-full"
onClick={() => handleAccept()}
>
Accept
</Button>
<Button
className="cursor-pointer w-full bg-blue-500 hover:bg-blue-500/90"
onClick={() => handleAccept(true)}
>
Accept, don&apos;t ask again
</Button>
</div>
)}
</div>
);
}

View File

@@ -19,9 +19,9 @@
"@langchain/core": "^0.3.41",
"@langchain/google-genai": "^0.1.10",
"@langchain/langgraph": "^0.2.49",
"@langchain/langgraph-api": "*",
"@langchain/langgraph-cli": "*",
"@langchain/langgraph-sdk": "*",
"@langchain/langgraph-api": "^0.0.14",
"@langchain/langgraph-cli": "^0.0.14",
"@langchain/langgraph-sdk": "^0.0.50",
"@langchain/openai": "^0.4.4",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
@@ -58,11 +58,6 @@
"uuid": "^11.0.5",
"zod": "^3.24.2"
},
"resolutions": {
"@langchain/langgraph-api": "0.0.14-experimental.1",
"@langchain/langgraph-cli": "0.0.14-experimental.1",
"@langchain/langgraph-sdk": "0.0.47-experimental.0"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/node": "^22.13.5",

364
pnpm-lock.yaml generated
View File

@@ -4,11 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
"@langchain/langgraph-api": 0.0.14-experimental.1
"@langchain/langgraph-cli": 0.0.14-experimental.1
"@langchain/langgraph-sdk": 0.0.47-experimental.0
importers:
.:
dependencies:
@@ -34,14 +29,14 @@ importers:
specifier: ^0.2.49
version: 0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
"@langchain/langgraph-api":
specifier: 0.0.14-experimental.1
version: 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
specifier: ^0.0.14
version: 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
"@langchain/langgraph-cli":
specifier: 0.0.14-experimental.1
version: 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
specifier: ^0.0.14
version: 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
"@langchain/langgraph-sdk":
specifier: 0.0.47-experimental.0
version: 0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
specifier: ^0.0.50
version: 0.0.50(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
"@langchain/openai":
specifier: ^0.4.4
version: 0.4.4(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))
@@ -437,15 +432,6 @@ packages:
integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==,
}
"@esbuild/aix-ppc64@0.23.1":
resolution:
{
integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==,
}
engines: { node: ">=18" }
cpu: [ppc64]
os: [aix]
"@esbuild/aix-ppc64@0.25.0":
resolution:
{
@@ -455,15 +441,6 @@ packages:
cpu: [ppc64]
os: [aix]
"@esbuild/android-arm64@0.23.1":
resolution:
{
integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [android]
"@esbuild/android-arm64@0.25.0":
resolution:
{
@@ -473,15 +450,6 @@ packages:
cpu: [arm64]
os: [android]
"@esbuild/android-arm@0.23.1":
resolution:
{
integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==,
}
engines: { node: ">=18" }
cpu: [arm]
os: [android]
"@esbuild/android-arm@0.25.0":
resolution:
{
@@ -491,15 +459,6 @@ packages:
cpu: [arm]
os: [android]
"@esbuild/android-x64@0.23.1":
resolution:
{
integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [android]
"@esbuild/android-x64@0.25.0":
resolution:
{
@@ -509,15 +468,6 @@ packages:
cpu: [x64]
os: [android]
"@esbuild/darwin-arm64@0.23.1":
resolution:
{
integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [darwin]
"@esbuild/darwin-arm64@0.25.0":
resolution:
{
@@ -527,15 +477,6 @@ packages:
cpu: [arm64]
os: [darwin]
"@esbuild/darwin-x64@0.23.1":
resolution:
{
integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [darwin]
"@esbuild/darwin-x64@0.25.0":
resolution:
{
@@ -545,15 +486,6 @@ packages:
cpu: [x64]
os: [darwin]
"@esbuild/freebsd-arm64@0.23.1":
resolution:
{
integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [freebsd]
"@esbuild/freebsd-arm64@0.25.0":
resolution:
{
@@ -563,15 +495,6 @@ packages:
cpu: [arm64]
os: [freebsd]
"@esbuild/freebsd-x64@0.23.1":
resolution:
{
integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [freebsd]
"@esbuild/freebsd-x64@0.25.0":
resolution:
{
@@ -581,15 +504,6 @@ packages:
cpu: [x64]
os: [freebsd]
"@esbuild/linux-arm64@0.23.1":
resolution:
{
integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [linux]
"@esbuild/linux-arm64@0.25.0":
resolution:
{
@@ -599,15 +513,6 @@ packages:
cpu: [arm64]
os: [linux]
"@esbuild/linux-arm@0.23.1":
resolution:
{
integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==,
}
engines: { node: ">=18" }
cpu: [arm]
os: [linux]
"@esbuild/linux-arm@0.25.0":
resolution:
{
@@ -617,15 +522,6 @@ packages:
cpu: [arm]
os: [linux]
"@esbuild/linux-ia32@0.23.1":
resolution:
{
integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==,
}
engines: { node: ">=18" }
cpu: [ia32]
os: [linux]
"@esbuild/linux-ia32@0.25.0":
resolution:
{
@@ -635,15 +531,6 @@ packages:
cpu: [ia32]
os: [linux]
"@esbuild/linux-loong64@0.23.1":
resolution:
{
integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==,
}
engines: { node: ">=18" }
cpu: [loong64]
os: [linux]
"@esbuild/linux-loong64@0.25.0":
resolution:
{
@@ -653,15 +540,6 @@ packages:
cpu: [loong64]
os: [linux]
"@esbuild/linux-mips64el@0.23.1":
resolution:
{
integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==,
}
engines: { node: ">=18" }
cpu: [mips64el]
os: [linux]
"@esbuild/linux-mips64el@0.25.0":
resolution:
{
@@ -671,15 +549,6 @@ packages:
cpu: [mips64el]
os: [linux]
"@esbuild/linux-ppc64@0.23.1":
resolution:
{
integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==,
}
engines: { node: ">=18" }
cpu: [ppc64]
os: [linux]
"@esbuild/linux-ppc64@0.25.0":
resolution:
{
@@ -689,15 +558,6 @@ packages:
cpu: [ppc64]
os: [linux]
"@esbuild/linux-riscv64@0.23.1":
resolution:
{
integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==,
}
engines: { node: ">=18" }
cpu: [riscv64]
os: [linux]
"@esbuild/linux-riscv64@0.25.0":
resolution:
{
@@ -707,15 +567,6 @@ packages:
cpu: [riscv64]
os: [linux]
"@esbuild/linux-s390x@0.23.1":
resolution:
{
integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==,
}
engines: { node: ">=18" }
cpu: [s390x]
os: [linux]
"@esbuild/linux-s390x@0.25.0":
resolution:
{
@@ -725,15 +576,6 @@ packages:
cpu: [s390x]
os: [linux]
"@esbuild/linux-x64@0.23.1":
resolution:
{
integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [linux]
"@esbuild/linux-x64@0.25.0":
resolution:
{
@@ -752,15 +594,6 @@ packages:
cpu: [arm64]
os: [netbsd]
"@esbuild/netbsd-x64@0.23.1":
resolution:
{
integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [netbsd]
"@esbuild/netbsd-x64@0.25.0":
resolution:
{
@@ -770,15 +603,6 @@ packages:
cpu: [x64]
os: [netbsd]
"@esbuild/openbsd-arm64@0.23.1":
resolution:
{
integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [openbsd]
"@esbuild/openbsd-arm64@0.25.0":
resolution:
{
@@ -788,15 +612,6 @@ packages:
cpu: [arm64]
os: [openbsd]
"@esbuild/openbsd-x64@0.23.1":
resolution:
{
integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [openbsd]
"@esbuild/openbsd-x64@0.25.0":
resolution:
{
@@ -806,15 +621,6 @@ packages:
cpu: [x64]
os: [openbsd]
"@esbuild/sunos-x64@0.23.1":
resolution:
{
integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [sunos]
"@esbuild/sunos-x64@0.25.0":
resolution:
{
@@ -824,15 +630,6 @@ packages:
cpu: [x64]
os: [sunos]
"@esbuild/win32-arm64@0.23.1":
resolution:
{
integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==,
}
engines: { node: ">=18" }
cpu: [arm64]
os: [win32]
"@esbuild/win32-arm64@0.25.0":
resolution:
{
@@ -842,15 +639,6 @@ packages:
cpu: [arm64]
os: [win32]
"@esbuild/win32-ia32@0.23.1":
resolution:
{
integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==,
}
engines: { node: ">=18" }
cpu: [ia32]
os: [win32]
"@esbuild/win32-ia32@0.25.0":
resolution:
{
@@ -860,15 +648,6 @@ packages:
cpu: [ia32]
os: [win32]
"@esbuild/win32-x64@0.23.1":
resolution:
{
integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==,
}
engines: { node: ">=18" }
cpu: [x64]
os: [win32]
"@esbuild/win32-x64@0.25.0":
resolution:
{
@@ -1093,10 +872,10 @@ packages:
peerDependencies:
"@langchain/core": ">=0.3.17 <0.4.0"
"@langchain/langgraph-api@0.0.14-experimental.1":
"@langchain/langgraph-api@0.0.14":
resolution:
{
integrity: sha512-gSQzZZk9tIrxXMQjudQbYHXPeK7l3Y/YbzCtnH6hWHvETQOZApUn0G18O5hWT9iYaAzZfSS8ExG7y6YM0MsFTQ==,
integrity: sha512-/lh6ug9kXBhL5zrX56MA4xxNt99kzLQqNuYqQRd2PWflVNATMRJNMfWhLjh91Hbn0yf3CWQoIX/6mPQiwCfrKg==,
}
engines: { node: ^18.19.0 || >=20.16.0 }
peerDependencies:
@@ -1114,18 +893,18 @@ packages:
peerDependencies:
"@langchain/core": ">=0.2.31 <0.4.0"
"@langchain/langgraph-cli@0.0.14-experimental.1":
"@langchain/langgraph-cli@0.0.14":
resolution:
{
integrity: sha512-S8Y7WrBPsNZR7wUyWj3De0sEdTTf+ipJf1lCrJho+moL9TVXUXUE+oFoMb1G/uHvt8Q/FCSE9BfadEg4JUb5MQ==,
integrity: sha512-wB6Q1VjAspGUXfbZnNuq56lXQNHHedqN09nfpGxNQnfnCf8VW/8veSkhCaNV5gdvRV9mDAWhJ0i78gxLxPhbRw==,
}
engines: { node: ^18.19.0 || >=20.16.0 }
hasBin: true
"@langchain/langgraph-sdk@0.0.47-experimental.0":
"@langchain/langgraph-sdk@0.0.50":
resolution:
{
integrity: sha512-di60Pi2knQbe/sjOB3gNbNQNuTIhj0Yjls0SfEYeWDHirSN9heumPB/oxvwaxLBA8JKhuHg2h5lKUxAIT4b+aA==,
integrity: sha512-gYL52WheZJ0U3xY09wu//+KNubaYoXEPxz0T1J/qJ0nyb/NtLwdFDhnC5TQhnhrFS4SrovbgsHEg7Qsr2VaS8g==,
}
peerDependencies:
"@langchain/core": ">=0.2.31 <0.4.0"
@@ -2871,14 +2650,6 @@ packages:
integrity: sha512-62CPYzyfcRE7OowGmWGKs9sz43QhCa/dZ5h6ruZhDg65B5Zsn++4EA4NKIwEMbAio9JV8+FJZNXzejNX/RjSkg==,
}
esbuild@0.23.1:
resolution:
{
integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==,
}
engines: { node: ">=18" }
hasBin: true
esbuild@0.25.0:
resolution:
{
@@ -5956,150 +5727,78 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
"@esbuild/aix-ppc64@0.23.1":
optional: true
"@esbuild/aix-ppc64@0.25.0":
optional: true
"@esbuild/android-arm64@0.23.1":
optional: true
"@esbuild/android-arm64@0.25.0":
optional: true
"@esbuild/android-arm@0.23.1":
optional: true
"@esbuild/android-arm@0.25.0":
optional: true
"@esbuild/android-x64@0.23.1":
optional: true
"@esbuild/android-x64@0.25.0":
optional: true
"@esbuild/darwin-arm64@0.23.1":
optional: true
"@esbuild/darwin-arm64@0.25.0":
optional: true
"@esbuild/darwin-x64@0.23.1":
optional: true
"@esbuild/darwin-x64@0.25.0":
optional: true
"@esbuild/freebsd-arm64@0.23.1":
optional: true
"@esbuild/freebsd-arm64@0.25.0":
optional: true
"@esbuild/freebsd-x64@0.23.1":
optional: true
"@esbuild/freebsd-x64@0.25.0":
optional: true
"@esbuild/linux-arm64@0.23.1":
optional: true
"@esbuild/linux-arm64@0.25.0":
optional: true
"@esbuild/linux-arm@0.23.1":
optional: true
"@esbuild/linux-arm@0.25.0":
optional: true
"@esbuild/linux-ia32@0.23.1":
optional: true
"@esbuild/linux-ia32@0.25.0":
optional: true
"@esbuild/linux-loong64@0.23.1":
optional: true
"@esbuild/linux-loong64@0.25.0":
optional: true
"@esbuild/linux-mips64el@0.23.1":
optional: true
"@esbuild/linux-mips64el@0.25.0":
optional: true
"@esbuild/linux-ppc64@0.23.1":
optional: true
"@esbuild/linux-ppc64@0.25.0":
optional: true
"@esbuild/linux-riscv64@0.23.1":
optional: true
"@esbuild/linux-riscv64@0.25.0":
optional: true
"@esbuild/linux-s390x@0.23.1":
optional: true
"@esbuild/linux-s390x@0.25.0":
optional: true
"@esbuild/linux-x64@0.23.1":
optional: true
"@esbuild/linux-x64@0.25.0":
optional: true
"@esbuild/netbsd-arm64@0.25.0":
optional: true
"@esbuild/netbsd-x64@0.23.1":
optional: true
"@esbuild/netbsd-x64@0.25.0":
optional: true
"@esbuild/openbsd-arm64@0.23.1":
optional: true
"@esbuild/openbsd-arm64@0.25.0":
optional: true
"@esbuild/openbsd-x64@0.23.1":
optional: true
"@esbuild/openbsd-x64@0.25.0":
optional: true
"@esbuild/sunos-x64@0.23.1":
optional: true
"@esbuild/sunos-x64@0.25.0":
optional: true
"@esbuild/win32-arm64@0.23.1":
optional: true
"@esbuild/win32-arm64@0.25.0":
optional: true
"@esbuild/win32-ia32@0.23.1":
optional: true
"@esbuild/win32-ia32@0.25.0":
optional: true
"@esbuild/win32-x64@0.23.1":
optional: true
"@esbuild/win32-x64@0.25.0":
optional: true
@@ -6243,7 +5942,7 @@ snapshots:
transitivePeerDependencies:
- zod
"@langchain/langgraph-api@0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
"@langchain/langgraph-api@0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
dependencies:
"@babel/code-frame": 7.26.2
"@hono/node-server": 1.13.8(hono@4.7.2)
@@ -6255,7 +5954,7 @@ snapshots:
"@typescript/vfs": 1.6.1(typescript@5.7.3)
dedent: 1.5.3
dotenv: 16.4.7
esbuild: 0.23.1
esbuild: 0.25.0
esbuild-plugin-tailwindcss: 2.0.1
exit-hook: 4.0.0
hono: 4.7.2
@@ -6279,11 +5978,11 @@ snapshots:
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
uuid: 10.0.0
"@langchain/langgraph-cli@0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
"@langchain/langgraph-cli@0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)":
dependencies:
"@babel/code-frame": 7.26.2
"@commander-js/extra-typings": 13.1.0(commander@13.1.0)
"@langchain/langgraph-api": 0.0.14-experimental.1(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
"@langchain/langgraph-api": 0.0.14(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(@langchain/langgraph-checkpoint@0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2))))(@langchain/langgraph@0.2.49(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0))(openai@4.85.4(zod@3.24.2))(typescript@5.7.3)
chokidar: 4.0.3
commander: 13.1.0
dedent: 1.5.3
@@ -6308,7 +6007,7 @@ snapshots:
- supports-color
- typescript
"@langchain/langgraph-sdk@0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)":
"@langchain/langgraph-sdk@0.0.50(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)":
dependencies:
"@types/json-schema": 7.0.15
p-queue: 6.6.2
@@ -6322,7 +6021,7 @@ snapshots:
dependencies:
"@langchain/core": 0.3.41(openai@4.85.4(zod@3.24.2))
"@langchain/langgraph-checkpoint": 0.0.15(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))
"@langchain/langgraph-sdk": 0.0.47-experimental.0(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
"@langchain/langgraph-sdk": 0.0.50(@langchain/core@0.3.41(openai@4.85.4(zod@3.24.2)))(react@19.0.0)
uuid: 10.0.0
zod: 3.24.2
transitivePeerDependencies:
@@ -7296,33 +6995,6 @@ snapshots:
postcss: 8.5.3
postcss-modules: 6.0.1(postcss@8.5.3)
esbuild@0.23.1:
optionalDependencies:
"@esbuild/aix-ppc64": 0.23.1
"@esbuild/android-arm": 0.23.1
"@esbuild/android-arm64": 0.23.1
"@esbuild/android-x64": 0.23.1
"@esbuild/darwin-arm64": 0.23.1
"@esbuild/darwin-x64": 0.23.1
"@esbuild/freebsd-arm64": 0.23.1
"@esbuild/freebsd-x64": 0.23.1
"@esbuild/linux-arm": 0.23.1
"@esbuild/linux-arm64": 0.23.1
"@esbuild/linux-ia32": 0.23.1
"@esbuild/linux-loong64": 0.23.1
"@esbuild/linux-mips64el": 0.23.1
"@esbuild/linux-ppc64": 0.23.1
"@esbuild/linux-riscv64": 0.23.1
"@esbuild/linux-s390x": 0.23.1
"@esbuild/linux-x64": 0.23.1
"@esbuild/netbsd-x64": 0.23.1
"@esbuild/openbsd-arm64": 0.23.1
"@esbuild/openbsd-x64": 0.23.1
"@esbuild/sunos-x64": 0.23.1
"@esbuild/win32-arm64": 0.23.1
"@esbuild/win32-ia32": 0.23.1
"@esbuild/win32-x64": 0.23.1
esbuild@0.25.0:
optionalDependencies:
"@esbuild/aix-ppc64": 0.25.0

View File

@@ -18,29 +18,28 @@ function CustomComponent({
const [apiUrl] = useQueryParam("apiUrl", StringParam);
const meta = thread.getMessagesMetadata(message);
const seenState = meta?.firstSeenState;
const customComponent = seenState?.values.ui
const customComponents = seenState?.values.ui
?.slice()
.reverse()
.find(
.filter(
({ additional_kwargs }) =>
additional_kwargs.run_id === seenState.metadata?.run_id,
additional_kwargs.run_id === seenState.metadata?.run_id &&
(!additional_kwargs.message_id ||
additional_kwargs.message_id === message.id),
);
if (!customComponent) {
return null;
}
if (!customComponents?.length) return null;
return (
<div key={message.id}>
{customComponent && (
{customComponents.map((customComponent) => (
<LoadExternalComponent
key={customComponent.id}
apiUrl={apiUrl ?? undefined}
assistantId="agent"
stream={thread}
message={customComponent}
meta={{ ui: customComponent }}
/>
)}
))}
</div>
);
}

View File

@@ -84,7 +84,9 @@ export function HumanMessage({
onSubmit={handleSubmitEdit}
/>
) : (
<p className="text-right px-4 py-2 rounded-3xl bg-muted">{contentString}</p>
<p className="text-right px-4 py-2 rounded-3xl bg-muted">
{contentString}
</p>
)}
<div

View File

@@ -15,7 +15,7 @@ export function ToolCalls({
if (!toolCalls || toolCalls.length === 0) return null;
return (
<div className="space-y-4">
<div className="space-y-4 w-full max-w-4xl">
{toolCalls.map((tc, idx) => {
const args = tc.args as Record<string, any>;
const hasArgs = Object.keys(args).length > 0;