This commit is contained in:
bracesproul
2025-03-07 11:21:49 -08:00
parent 7ebcbb3a28
commit c338522a34
4 changed files with 132 additions and 27 deletions

View File

@@ -13,17 +13,19 @@ export async function executor(
): Promise<OpenCodeUpdate> { ): Promise<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(config); const ui = typedUi<typeof ComponentMap>(config);
const numOfUpdateFileCalls = state.messages.filter( const lastPlanToolCall = state.messages.findLast(
(m) => (m) =>
m.getType() === "ai" && m.getType() === "ai" &&
(m as unknown as AIMessage).tool_calls?.some( (m as unknown as AIMessage).tool_calls?.some((tc) => tc.name === "plan"),
(tc) => tc.name === "update_file", ) as AIMessage | undefined;
), const planToolCallArgs = lastPlanToolCall?.tool_calls?.[0]?.args?.args;
).length; const numOfExecutedPlanItems: number =
const planItem = PLAN[numOfUpdateFileCalls - 1]; planToolCallArgs?.executedPlans?.length ?? 0;
const planItem = PLAN[numOfExecutedPlanItems - 1];
let updateFileContents = ""; let updateFileContents = "";
switch (numOfUpdateFileCalls) { switch (numOfExecutedPlanItems) {
case 0: case 0:
updateFileContents = await fs.readFile( updateFileContents = await fs.readFile(
"agent/open-code/nodes/plan-code/step-1.txt", "agent/open-code/nodes/plan-code/step-1.txt",

View File

@@ -16,22 +16,37 @@ export const PLAN = [
]; ];
export async function planner( export async function planner(
_state: OpenCodeState, state: OpenCodeState,
config: LangGraphRunnableConfig, config: LangGraphRunnableConfig,
): Promise<OpenCodeUpdate> { ): Promise<OpenCodeUpdate> {
const ui = typedUi<typeof ComponentMap>(config); 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?.args;
const executedPlans = planToolCallArgs?.executedPlans ?? [];
const remainingPlans = planToolCallArgs?.remainingPlans ?? PLAN;
const content =
executedPlans.length > 0
? `I've updated the plan list based on the executed plans.`
: `I've come up with a detailed plan for building the todo app.`;
const toolCallId = uuidv4(); const toolCallId = uuidv4();
const aiMessage: AIMessage = { const aiMessage: AIMessage = {
type: "ai", type: "ai",
id: uuidv4(), id: uuidv4(),
content: "I've come up with a detailed plan for building the todo app.", content,
tool_calls: [ tool_calls: [
{ {
name: "plan", name: "plan",
args: { args: {
args: { args: {
plan: PLAN, executedPlans,
remainingPlans,
}, },
}, },
id: toolCallId, id: toolCallId,
@@ -42,7 +57,8 @@ export async function planner(
ui.write("code-plan", { ui.write("code-plan", {
toolCallId, toolCallId,
plan: PLAN, executedPlans,
remainingPlans,
}); });
const toolMessage: ToolMessage = { const toolMessage: ToolMessage = {

View File

@@ -2,20 +2,36 @@ import "./index.css";
interface PlanProps { interface PlanProps {
toolCallId: string; toolCallId: string;
plan: string[]; executedPlans: string[];
remainingPlans: string[];
} }
export default function Plan(props: PlanProps) { export default function Plan(props: PlanProps) {
return ( return (
<div className="flex flex-col gap-4 w-full max-w-xl p-4 border-[1px] rounded-xl border-slate-500"> <div className="flex flex-col gap-4 w-full max-w-xl p-6 border-[1px] rounded-xl border-slate-500">
<p className="text-lg font-medium">Plan</p> <h2 className="text-2xl font-semibold text-center mb-2">Code Plan</h2>
<div className="flex flex-col gap-2"> <div className="grid grid-cols-2 divide-x divide-slate-300">
{props.plan.map((step, index) => ( <div className="flex flex-col gap-2 pr-6">
<p key={index} className="font-mono"> <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">
{index + 1}. {step} {index + 1}. {step}
</p> </p>
))} ))}
</div> </div>
<div className="flex flex-col gap-2 pl-6">
<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">
{props.executedPlans.length + index + 1}. {step}
</p>
))}
</div>
</div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,13 @@
import { Button } from "@/components/ui/button";
import "./index.css"; import "./index.css";
import { v4 as uuidv4 } from "uuid";
import { Button } from "@/components/ui/button";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; 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 { useState } from "react";
interface ProposedChangeProps { interface ProposedChangeProps {
toolCallId: string; toolCallId: string;
@@ -11,12 +16,72 @@ interface ProposedChangeProps {
} }
export default function ProposedChange(props: ProposedChangeProps) { export default function ProposedChange(props: ProposedChangeProps) {
const handleReject = () => {}; const [isAccepted, setIsAccepted] = useState(false);
const handleAccept = () => {};
const thread = useStreamContext<
{ messages: Message[]; ui: UIMessage[] },
{ MetaType: { ui: UIMessage | undefined } }
>();
const handleReject = () => {
alert("Rejected. (just kidding, you can't reject me silly!)");
};
const handleAccept = () => {
const content = "User accepted the proposed change. Please continue.";
thread.submit({
messages: [
{
type: "tool",
tool_call_id: props.toolCallId,
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
name: "buy-stock",
content,
},
{
type: "human",
content: `Accepted change.`,
},
],
});
setIsAccepted(true);
};
if (isAccepted) {
return (
<div className="flex flex-col gap-4 w-full max-w-xl p-4 border-[1px] rounded-xl border-green-300">
<div className="flex flex-col items-start justify-start gap-2">
<p className="text-lg font-medium">Accepted 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 ( return (
<div className="flex flex-col gap-4 w-full max-w-xl p-4 border-[1px] rounded-xl border-slate-200"> <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-lg font-medium">Proposed Change</p>
<p className="text-sm font-mono">{props.planItem}</p>
</div>
<ReactMarkdown <ReactMarkdown
children={props.change} children={props.change}
components={{ components={{
@@ -36,10 +101,16 @@ export default function ProposedChange(props: ProposedChangeProps) {
}} }}
/> />
<div className="flex gap-2 items-center w-full"> <div className="flex gap-2 items-center w-full">
<Button variant="destructive" onClick={handleReject}> <Button
className="cursor-pointer"
variant="destructive"
onClick={handleReject}
>
Reject Reject
</Button> </Button>
<Button onClick={handleAccept}>Accept</Button> <Button className="cursor-pointer" onClick={handleAccept}>
Accept
</Button>
</div> </div>
</div> </div>
); );