mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 17:02:14 +00:00
[open-swe] feat: add accepted plan action component for planner interface (#335)
* Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * fix build and accepted-plan-component rendering * CR * improve component UI design * overflow scroll on accepted plan component * cr * Update apps/web/src/components/v2/actions-renderer.tsx * fix: styling --------- Co-authored-by: open-swe-dev[bot] <open-swe-dev@users.noreply.github.com> Co-authored-by: starmorph <dylan@starmorph.com> Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
parent
5a8e6d0656
commit
19206f0c34
4 changed files with 352 additions and 15 deletions
|
|
@ -1,9 +1,11 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||||
import { Command, END, interrupt } from "@langchain/langgraph";
|
import { Command, END, interrupt } from "@langchain/langgraph";
|
||||||
import {
|
import {
|
||||||
GraphUpdate,
|
GraphUpdate,
|
||||||
GraphConfig,
|
GraphConfig,
|
||||||
TaskPlan,
|
TaskPlan,
|
||||||
|
PlanItem,
|
||||||
} from "@open-swe/shared/open-swe/types";
|
} from "@open-swe/shared/open-swe/types";
|
||||||
import {
|
import {
|
||||||
ActionRequest,
|
ActionRequest,
|
||||||
|
|
@ -20,6 +22,7 @@ import {
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
PLAN_INTERRUPT_ACTION_TITLE,
|
PLAN_INTERRUPT_ACTION_TITLE,
|
||||||
PLAN_INTERRUPT_DELIMITER,
|
PLAN_INTERRUPT_DELIMITER,
|
||||||
|
DO_NOT_RENDER_ID_PREFIX,
|
||||||
PROGRAMMER_GRAPH_ID,
|
PROGRAMMER_GRAPH_ID,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,15 +32,50 @@ import {
|
||||||
import { createLangGraphClient } from "../../../utils/langgraph-client.js";
|
import { createLangGraphClient } from "../../../utils/langgraph-client.js";
|
||||||
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js";
|
||||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||||
|
import {
|
||||||
|
ACCEPTED_PLAN_NODE_ID,
|
||||||
|
CustomNodeEvent,
|
||||||
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
||||||
|
|
||||||
|
function createAcceptedPlanMessage(input: {
|
||||||
|
planTitle: string;
|
||||||
|
planItems: PlanItem[];
|
||||||
|
interruptType: HumanResponse["type"];
|
||||||
|
}) {
|
||||||
|
const { planTitle, planItems, interruptType } = input;
|
||||||
|
const acceptedPlanEvent: CustomNodeEvent = {
|
||||||
|
nodeId: ACCEPTED_PLAN_NODE_ID,
|
||||||
|
actionId: uuidv4(),
|
||||||
|
action: "Plan accepted",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
status: "success",
|
||||||
|
planTitle,
|
||||||
|
planItems,
|
||||||
|
interruptType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptedPlanMessage = new AIMessage({
|
||||||
|
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
|
||||||
|
content: "",
|
||||||
|
additional_kwargs: {
|
||||||
|
hidden: true,
|
||||||
|
customNodeEvents: [acceptedPlanEvent],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return acceptedPlanMessage;
|
||||||
|
}
|
||||||
|
|
||||||
async function startProgrammerRun(input: {
|
async function startProgrammerRun(input: {
|
||||||
runInput: Exclude<GraphUpdate, "taskPlan"> & { taskPlan: TaskPlan };
|
runInput: Exclude<GraphUpdate, "taskPlan"> & { taskPlan: TaskPlan };
|
||||||
state: PlannerGraphState;
|
state: PlannerGraphState;
|
||||||
config: GraphConfig;
|
config: GraphConfig;
|
||||||
|
newMessages?: BaseMessage[];
|
||||||
}) {
|
}) {
|
||||||
const { runInput, state, config } = input;
|
const { runInput, state, config, newMessages } = input;
|
||||||
const langGraphClient = createLangGraphClient({
|
const langGraphClient = createLangGraphClient({
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
[GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "",
|
[GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "",
|
||||||
|
|
@ -83,6 +121,7 @@ async function startProgrammerRun(input: {
|
||||||
},
|
},
|
||||||
sandboxSessionId: runInput.sandboxSessionId,
|
sandboxSessionId: runInput.sandboxSessionId,
|
||||||
taskPlan: runInput.taskPlan,
|
taskPlan: runInput.taskPlan,
|
||||||
|
messages: newMessages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +134,7 @@ export async function interruptProposedPlan(
|
||||||
throw new Error("No proposed plan found.");
|
throw new Error("No proposed plan found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let planItems: PlanItem[];
|
||||||
const userRequest = getUserRequest(state.messages);
|
const userRequest = getUserRequest(state.messages);
|
||||||
const runInput: GraphUpdate = {
|
const runInput: GraphUpdate = {
|
||||||
contextGatheringNotes: state.contextGatheringNotes,
|
contextGatheringNotes: state.contextGatheringNotes,
|
||||||
|
|
@ -105,7 +145,7 @@ export async function interruptProposedPlan(
|
||||||
|
|
||||||
if (state.autoAcceptPlan) {
|
if (state.autoAcceptPlan) {
|
||||||
logger.info("Auto accepting plan.");
|
logger.info("Auto accepting plan.");
|
||||||
const planItems = proposedPlan.map((p, index) => ({
|
planItems = proposedPlan.map((p, index) => ({
|
||||||
index,
|
index,
|
||||||
plan: p,
|
plan: p,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
|
@ -123,6 +163,13 @@ export async function interruptProposedPlan(
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
config,
|
config,
|
||||||
|
newMessages: [
|
||||||
|
createAcceptedPlanMessage({
|
||||||
|
planTitle: state.proposedPlanTitle,
|
||||||
|
planItems,
|
||||||
|
interruptType: "accept",
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +208,7 @@ export async function interruptProposedPlan(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interruptRes.type === "accept") {
|
if (interruptRes.type === "accept") {
|
||||||
const planItems = proposedPlan.map((p, index) => ({
|
planItems = proposedPlan.map((p, index) => ({
|
||||||
index,
|
index,
|
||||||
plan: p,
|
plan: p,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
|
@ -178,7 +225,7 @@ export async function interruptProposedPlan(
|
||||||
.split(PLAN_INTERRUPT_DELIMITER)
|
.split(PLAN_INTERRUPT_DELIMITER)
|
||||||
.map((step: string) => step.trim());
|
.map((step: string) => step.trim());
|
||||||
|
|
||||||
const planItems = editedPlan.map((p: string, index: number) => ({
|
planItems = editedPlan.map((p: string, index: number) => ({
|
||||||
index,
|
index,
|
||||||
plan: p,
|
plan: p,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
|
@ -200,5 +247,12 @@ export async function interruptProposedPlan(
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
config,
|
config,
|
||||||
|
newMessages: [
|
||||||
|
createAcceptedPlanMessage({
|
||||||
|
planTitle: state.proposedPlanTitle,
|
||||||
|
planItems,
|
||||||
|
interruptType: interruptRes.type,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
204
apps/web/src/components/gen-ui/accepted-plan-step.tsx
Normal file
204
apps/web/src/components/gen-ui/accepted-plan-step.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CheckCircle,
|
||||||
|
ChevronDown,
|
||||||
|
Sparkles,
|
||||||
|
Circle,
|
||||||
|
Check,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
|
|
||||||
|
type PlanItem = {
|
||||||
|
index: number;
|
||||||
|
plan: string;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AcceptedPlanStepProps = {
|
||||||
|
planTitle?: string;
|
||||||
|
planItems?: PlanItem[];
|
||||||
|
interruptType?: HumanResponse["type"];
|
||||||
|
collapse?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AcceptedPlanStep({
|
||||||
|
planTitle,
|
||||||
|
planItems = [],
|
||||||
|
interruptType,
|
||||||
|
collapse: collapseProp = true,
|
||||||
|
}: AcceptedPlanStepProps) {
|
||||||
|
const [collapsed, setCollapsed] = useState(collapseProp);
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (interruptType === "edit") {
|
||||||
|
return "Plan revised and approved";
|
||||||
|
}
|
||||||
|
return "Plan approved and ready";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = () => {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="border-emerald-200 bg-emerald-100 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
Approved
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlanItemIcon = (item: PlanItem) => {
|
||||||
|
if (item.completed) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-950">
|
||||||
|
<CheckCircle className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="bg-muted flex h-6 w-6 items-center justify-center rounded-full">
|
||||||
|
<Circle className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalCount = planItems.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group via-background to-background dark:via-background dark:to-background rounded-xl border bg-gradient-to-br from-emerald-50/50 transition-shadow dark:from-emerald-950/20",
|
||||||
|
!collapsed ? "shadow-sm hover:shadow-md" : "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex items-center border-b bg-gradient-to-r from-emerald-50 to-emerald-50/50 p-4 backdrop-blur-sm dark:from-emerald-950/30 dark:to-emerald-950/10",
|
||||||
|
!collapsed ? "rounded-t-xl rounded-b-none" : "rounded-xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-500 shadow-md dark:bg-emerald-600">
|
||||||
|
<Sparkles className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-foreground text-sm font-semibold">
|
||||||
|
{getStatusText()}
|
||||||
|
</h3>
|
||||||
|
{getStatusBadge()}
|
||||||
|
</div>
|
||||||
|
{totalCount > 0 && (
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{totalCount} steps
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
aria-label={
|
||||||
|
collapsed ? "Expand plan details" : "Collapse plan details"
|
||||||
|
}
|
||||||
|
onClick={() => setCollapsed((c) => !c)}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0 hover:bg-emerald-100 dark:hover:bg-emerald-950/50"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4 transition-transform duration-200",
|
||||||
|
collapsed ? "rotate-0" : "rotate-180",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300 ease-in-out",
|
||||||
|
collapsed ? "hidden" : "flex",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
{planTitle && (
|
||||||
|
<div className="bg-card/50 rounded-lg border p-3">
|
||||||
|
<h4 className="text-foreground mb-1 text-sm font-medium">
|
||||||
|
Plan Overview
|
||||||
|
</h4>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
{planTitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{planItems.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-foreground flex items-center gap-2 text-sm font-medium">
|
||||||
|
Execution Steps
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{totalCount}
|
||||||
|
</Badge>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{planItems
|
||||||
|
.sort((a, b) => a.index - b.index)
|
||||||
|
.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={item.index}
|
||||||
|
className={cn(
|
||||||
|
"group/item flex items-start gap-3 rounded-lg p-3 transition-colors duration-200",
|
||||||
|
item.completed
|
||||||
|
? "bg-emerald-50/50 dark:bg-emerald-950/20"
|
||||||
|
: "bg-muted/30 hover:bg-muted/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mt-0.5 flex-shrink-0">
|
||||||
|
{getPlanItemIcon(item)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-sm leading-relaxed",
|
||||||
|
item.completed
|
||||||
|
? "text-foreground"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.plan}
|
||||||
|
</p>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"flex-shrink-0 text-xs",
|
||||||
|
item.completed &&
|
||||||
|
"border-emerald-200 text-emerald-700 dark:border-emerald-800 dark:text-emerald-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{idx + 1}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,13 +7,56 @@ import {
|
||||||
isCustomNodeEvent,
|
isCustomNodeEvent,
|
||||||
CustomNodeEvent,
|
CustomNodeEvent,
|
||||||
INITIALIZE_NODE_ID,
|
INITIALIZE_NODE_ID,
|
||||||
|
ACCEPTED_PLAN_NODE_ID,
|
||||||
mapCustomEventsToSteps,
|
mapCustomEventsToSteps,
|
||||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
import {
|
||||||
|
DO_NOT_RENDER_ID_PREFIX,
|
||||||
|
PLANNER_GRAPH_ID,
|
||||||
|
} from "@open-swe/shared/constants";
|
||||||
import { Message } from "@langchain/langgraph-sdk";
|
import { Message } from "@langchain/langgraph-sdk";
|
||||||
import { InitializeStep } from "../gen-ui/initialize-step";
|
import { InitializeStep } from "../gen-ui/initialize-step";
|
||||||
|
import { AcceptedPlanStep } from "../gen-ui/accepted-plan-step";
|
||||||
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, PlanItem } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { HumanResponse } from "@langchain/langgraph/prebuilt";
|
||||||
|
|
||||||
|
interface AcceptedPlanEventData {
|
||||||
|
planTitle: string;
|
||||||
|
planItems: PlanItem[];
|
||||||
|
interruptType: HumanResponse["type"];
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcceptedPlanEvent = CustomNodeEvent & {
|
||||||
|
data: AcceptedPlanEventData;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAcceptedPlanEvent(
|
||||||
|
event: CustomNodeEvent,
|
||||||
|
): event is AcceptedPlanEvent {
|
||||||
|
const { data } = event;
|
||||||
|
return (
|
||||||
|
typeof data === "object" &&
|
||||||
|
data !== null &&
|
||||||
|
typeof data.planTitle === "string" &&
|
||||||
|
Array.isArray(data.planItems) &&
|
||||||
|
data.planItems.every(
|
||||||
|
(item) =>
|
||||||
|
typeof item === "object" &&
|
||||||
|
item !== null &&
|
||||||
|
typeof item.index === "number" &&
|
||||||
|
typeof item.plan === "string" &&
|
||||||
|
typeof item.completed === "boolean",
|
||||||
|
) &&
|
||||||
|
(data.interruptType === "accept" || data.interruptType === "edit")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAcceptedPlanEvents(
|
||||||
|
events: CustomNodeEvent[],
|
||||||
|
): events is AcceptedPlanEvent[] {
|
||||||
|
return events.every(isAcceptedPlanEvent);
|
||||||
|
}
|
||||||
|
|
||||||
interface ActionsRendererProps {
|
interface ActionsRendererProps {
|
||||||
graphId: string;
|
graphId: string;
|
||||||
|
|
@ -74,6 +117,11 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
||||||
const initializeEvents = customNodeEvents.filter(
|
const initializeEvents = customNodeEvents.filter(
|
||||||
(e) => e.nodeId === INITIALIZE_NODE_ID,
|
(e) => e.nodeId === INITIALIZE_NODE_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const acceptedPlanEvents = customNodeEvents.filter(
|
||||||
|
(e) => e.nodeId === ACCEPTED_PLAN_NODE_ID,
|
||||||
|
);
|
||||||
|
|
||||||
const steps = mapCustomEventsToSteps(initializeEvents);
|
const steps = mapCustomEventsToSteps(initializeEvents);
|
||||||
const allSuccess =
|
const allSuccess =
|
||||||
steps.length > 0 && steps.every((s) => s.status === "success");
|
steps.length > 0 && steps.every((s) => s.status === "success");
|
||||||
|
|
@ -84,15 +132,29 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const customInitEvents = getCustomNodeEventsFromMessages(
|
const allCustomEvents = getCustomNodeEventsFromMessages(stream.messages);
|
||||||
stream.messages,
|
if (!allCustomEvents?.length) {
|
||||||
INITIALIZE_NODE_ID,
|
|
||||||
);
|
|
||||||
// If there are no custom init events found in messages, or we already have steps from custom events, return
|
|
||||||
if (!customInitEvents?.length || initializeEvents.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCustomNodeEvents(customInitEvents);
|
|
||||||
|
setCustomNodeEvents((prev) => {
|
||||||
|
// If no existing events, set all new events
|
||||||
|
if (prev.length === 0) {
|
||||||
|
return allCustomEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge new events with existing ones, avoiding duplicates
|
||||||
|
const existingActionIds = new Set(prev.map((e) => e.actionId));
|
||||||
|
const newEvents = allCustomEvents.filter(
|
||||||
|
(e) => !existingActionIds.has(e.actionId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newEvents.length > 0) {
|
||||||
|
return [...prev, ...newEvents];
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
}, [stream.messages]);
|
}, [stream.messages]);
|
||||||
|
|
||||||
const streamJoined = useRef(false);
|
const streamJoined = useRef(false);
|
||||||
|
|
@ -122,9 +184,17 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
||||||
programmerSession?.threadId)
|
programmerSession?.threadId)
|
||||||
) {
|
) {
|
||||||
setProgrammerSession?.(stream.values.programmerSession);
|
setProgrammerSession?.(stream.values.programmerSession);
|
||||||
setSelectedTab?.("programmer");
|
|
||||||
|
// Only switch tabs from the planner ActionsRenderer to ensure proper timing
|
||||||
|
// This allows the accepted plan step to be visible before switching
|
||||||
|
if (graphId === PLANNER_GRAPH_ID) {
|
||||||
|
// Add a small delay to allow the accepted plan step to render first
|
||||||
|
setTimeout(() => {
|
||||||
|
setSelectedTab?.("programmer");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [stream.values]);
|
}, [stream.values, graphId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
|
@ -145,6 +215,14 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
|
||||||
handleRegenerate={() => {}}
|
handleRegenerate={() => {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{acceptedPlanEvents.length > 0 &&
|
||||||
|
isAcceptedPlanEvents(acceptedPlanEvents) && (
|
||||||
|
<AcceptedPlanStep
|
||||||
|
planTitle={acceptedPlanEvents[0].data.planTitle}
|
||||||
|
planItems={acceptedPlanEvents[0].data.planItems}
|
||||||
|
interruptType={acceptedPlanEvents[0].data.interruptType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export function isCustomNodeEvent(event: unknown): event is CustomNodeEvent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export const INITIALIZE_NODE_ID = "initialize";
|
export const INITIALIZE_NODE_ID = "initialize";
|
||||||
|
export const ACCEPTED_PLAN_NODE_ID = "accepted-plan";
|
||||||
|
|
||||||
export const INIT_STEPS = [
|
export const INIT_STEPS = [
|
||||||
"Resuming sandbox",
|
"Resuming sandbox",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue