diff --git a/agent/agent.ts b/agent/agent.ts index dfacb6f..30f3db9 100644 --- a/agent/agent.ts +++ b/agent/agent.ts @@ -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(); diff --git a/agent/open-code/index.ts b/agent/open-code/index.ts new file mode 100644 index 0000000..2707cff --- /dev/null +++ b/agent/open-code/index.ts @@ -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"; diff --git a/agent/open-code/nodes/executor.ts b/agent/open-code/nodes/executor.ts new file mode 100644 index 0000000..90e6be7 --- /dev/null +++ b/agent/open-code/nodes/executor.ts @@ -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 { + const ui = typedUi(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(), + }; +} diff --git a/agent/open-code/nodes/plan-code/step-1.txt b/agent/open-code/nodes/plan-code/step-1.txt new file mode 100644 index 0000000..e4776f4 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-1.txt @@ -0,0 +1,5 @@ +```bash +npx create-react-app todo-app --template typescript +cd todo-app +mkdir -p src/{components,styles,utils} +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-2.txt b/agent/open-code/nodes/plan-code/step-2.txt new file mode 100644 index 0000000..93c51c9 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-2.txt @@ -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 = ({ id, text, completed, onToggle, onDelete }) => ( +
+ onToggle(id)} /> + {text} + +
+); +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-3.txt b/agent/open-code/nodes/plan-code/step-3.txt new file mode 100644 index 0000000..3e6d746 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-3.txt @@ -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; +} | undefined>(undefined); + +export const TodoProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, dispatch] = useReducer(todoReducer, { todos: [] }); + return {children}; +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-4.txt b/agent/open-code/nodes/plan-code/step-4.txt new file mode 100644 index 0000000..b3c2a37 --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-4.txt @@ -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 ( +
+ setText(e.target.value)} + placeholder='Add a new todo' + /> + {error &&
{error}
} + +
+ ); +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-5.txt b/agent/open-code/nodes/plan-code/step-5.txt new file mode 100644 index 0000000..2082d9d --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-5.txt @@ -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 }) => ( +
+ + + +
+); +``` \ No newline at end of file diff --git a/agent/open-code/nodes/plan-code/step-6.txt b/agent/open-code/nodes/plan-code/step-6.txt new file mode 100644 index 0000000..84d96fa --- /dev/null +++ b/agent/open-code/nodes/plan-code/step-6.txt @@ -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) : []; +}; +``` \ No newline at end of file diff --git a/agent/open-code/nodes/planner.ts b/agent/open-code/nodes/planner.ts new file mode 100644 index 0000000..1850988 --- /dev/null +++ b/agent/open-code/nodes/planner.ts @@ -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 { + const ui = typedUi(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(), + }; +} diff --git a/agent/open-code/types.ts b/agent/open-code/types.ts new file mode 100644 index 0000000..3a32290 --- /dev/null +++ b/agent/open-code/types.ts @@ -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; diff --git a/agent/types.ts b/agent/types.ts index 20f7017..74c0f47 100644 --- a/agent/types.ts +++ b/agent/types.ts @@ -12,7 +12,9 @@ export const GenerativeUIAnnotation = Annotation.Root({ UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[] >({ default: () => [], reducer: uiMessageReducer }), timestamp: Annotation, - next: Annotation<"stockbroker" | "tripPlanner" | "generalInput">(), + next: Annotation< + "stockbroker" | "tripPlanner" | "openCode" | "generalInput" + >(), }); export type GenerativeUIState = typeof GenerativeUIAnnotation.State; diff --git a/agent/uis/index.tsx b/agent/uis/index.tsx index 89d151c..f932995 100644 --- a/agent/uis/index.tsx +++ b/agent/uis/index.tsx @@ -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; diff --git a/agent/uis/open-code/plan/index.css b/agent/uis/open-code/plan/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/agent/uis/open-code/plan/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/agent/uis/open-code/plan/index.tsx b/agent/uis/open-code/plan/index.tsx new file mode 100644 index 0000000..2b524ee --- /dev/null +++ b/agent/uis/open-code/plan/index.tsx @@ -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 ( +
+
+

Code Plan

+
+ +
+
+

+ Remaining Plans +

+ {props.remainingPlans.map((step, index) => ( +

+ {index + 1}. {step} +

+ ))} +
+
+

+ Executed Plans +

+ {props.executedPlans.map((step, index) => ( +

+ {step} +

+ ))} +
+
+

+ Rejected Plans +

+ {props.rejectedPlans.map((step, index) => ( +

+ {step} +

+ ))} +
+
+
+ setIsExpanded(!isExpanded)} + animate={{ rotate: isExpanded ? 180 : 0 }} + transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }} + > + + +
+ ); +} diff --git a/agent/uis/open-code/proposed-change/index.css b/agent/uis/open-code/proposed-change/index.css new file mode 100644 index 0000000..e69504a --- /dev/null +++ b/agent/uis/open-code/proposed-change/index.css @@ -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; + } +} diff --git a/agent/uis/open-code/proposed-change/index.tsx b/agent/uis/open-code/proposed-change/index.tsx new file mode 100644 index 0000000..402c00c --- /dev/null +++ b/agent/uis/open-code/proposed-change/index.tsx @@ -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 ( +
+
+

+ {isAccepted ? "Accepted" : "Rejected"} Change +

+

{props.planItem}

+
+ + ) : ( + {children} + ); + }, + }} + /> +
+ ); + } + + return ( +
+
+

Proposed Change

+

{props.planItem}

+
+ + ) : ( + {children} + ); + }, + }} + /> + {!props.fullWriteAccess && ( +
+ + + +
+ )} +
+ ); +} diff --git a/package.json b/package.json index 1f10fa0..20bde96 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 910fdff..db2daa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/components/thread/messages/ai.tsx b/src/components/thread/messages/ai.tsx index 607d113..5784841 100644 --- a/src/components/thread/messages/ai.tsx +++ b/src/components/thread/messages/ai.tsx @@ -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 (
- {customComponent && ( + {customComponents.map((customComponent) => ( - )} + ))}
); } diff --git a/src/components/thread/messages/human.tsx b/src/components/thread/messages/human.tsx index 7c0b553..a394874 100644 --- a/src/components/thread/messages/human.tsx +++ b/src/components/thread/messages/human.tsx @@ -84,7 +84,9 @@ export function HumanMessage({ onSubmit={handleSubmitEdit} /> ) : ( -

{contentString}

+

+ {contentString} +

)}
+
{toolCalls.map((tc, idx) => { const args = tc.args as Record; const hasArgs = Object.keys(args).length > 0;