Feat: Loading state action list (#360)

* init action-list loading state

* loading state logic

* rm comments

* simplify

* fix loading logic

* CR: DRY loadingActionsCard refactor

* hybrid loading logic; content + stream load

* fix duplicate loading actions text

---------

Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
Dylan Boudro 2025-07-08 12:47:06 -07:00 • committed by GitHub
parent 2a05332693
commit 23f72d0669
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 76 additions and 45 deletions

View file

@ -21,6 +21,7 @@ import { AcceptedPlanStep } from "../gen-ui/accepted-plan-step";
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { GraphState, PlanItem } from "@open-swe/shared/open-swe/types";
import { HumanResponse } from "@langchain/langgraph/prebuilt";
import { LoadingActionsCardContent } from "./thread-view-loading";
interface AcceptedPlanEventData {
planTitle: string;
@ -105,6 +106,8 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
const [customNodeEvents, setCustomNodeEvents] = useState<CustomNodeEvent[]>(
[],
);
const joinedRunId = useRef<string | undefined>(undefined);
const [streamLoading, setStreamLoading] = useState(false);
const stream = useStream<State>({
apiUrl: process.env.NEXT_PUBLIC_API_URL,
assistantId: graphId,
@ -167,15 +170,31 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
});
}, [stream.messages]);
const streamJoined = useRef(false);
// Clear streamLoading as soon as we get any content (agent has started running)
useEffect(() => {
if (!streamJoined.current && runId) {
streamJoined.current = true;
// TODO: If the SDK changes go in, use this instead:
// stream.joinStream(runId, undefined, { streamMode: ["values", "messages", "custom"]}).catch(console.error);
stream.joinStream(runId).catch(console.error);
const hasContent =
(stream.messages && stream.messages.length > 0) ||
customNodeEvents.length > 0;
if (hasContent && streamLoading) {
setStreamLoading(false);
}
}, [runId]);
}, [stream.messages, customNodeEvents, streamLoading]);
// TODO: If the SDK changes go in, use this instead:
// stream.joinStream(runId, undefined, { streamMode: ["values", "messages", "custom"]}).catch(console.error);
useEffect(() => {
if (runId && runId !== joinedRunId.current) {
joinedRunId.current = runId;
setStreamLoading(true);
stream
.joinStream(runId)
.catch(console.error)
.finally(() => setStreamLoading(false));
} else if (!runId) {
joinedRunId.current = undefined;
}
}, [runId, stream]);
useEffect(() => {
if (stream.isLoading) {
@ -214,6 +233,10 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
}
}, [stream.values, graphId]);
if (streamLoading) {
return <LoadingActionsCardContent />;
}
return (
<div className="flex w-full flex-col gap-2">
{initializeEvents.length > 0 && steps.length > 0 && (

View file

@ -8,9 +8,51 @@ interface ThreadViewLoadingProps {
onBackToHome?: () => void;
}
const isEven = (num: number) => num % 2 === 0;
function isEven(n: number) {
return n % 2 === 0;
}
export function LoadingActionsSection() {
export function LoadingActionsCardContent() {
return (
<div className="space-y-3">
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="border-border bg-muted/30 space-y-2 rounded-lg border p-3 dark:bg-gray-900"
>
{isEven(i) ? (
<div className="bg-background space-y-1 rounded p-2 dark:bg-black">
<div className="bg-muted h-2 w-32 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-2 w-48 animate-pulse rounded dark:bg-gray-700"></div>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-3 w-40 animate-pulse rounded dark:bg-gray-700"></div>
</div>
<div className="flex items-center gap-2">
<div className="bg-muted h-3 w-16 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
</div>
</div>
)}
</div>
))}
<div className="border-border space-y-2 rounded-lg border-2 border-dashed p-3">
<div className="bg-muted h-12 animate-pulse rounded dark:bg-gray-800"></div>
<div className="bg-muted h-7 w-16 animate-pulse rounded dark:bg-gray-800"></div>
</div>
<div className="flex gap-2 pt-3">
<div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
</div>
</div>
);
}
export function LoadingActionsCard() {
return (
<Card className="border-border bg-card dark:bg-gray-950">
<CardHeader className="p-3">
@ -24,41 +66,7 @@ export function LoadingActionsSection() {
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 p-3 pt-0">
{Array.from({ length: 8 }, (_, i) => (
<div
key={i}
className="border-border bg-muted/30 space-y-2 rounded-lg border p-3 dark:bg-gray-900"
>
{isEven(i) ? (
<div className="bg-background space-y-1 rounded p-2 dark:bg-black">
<div className="bg-muted h-2 w-32 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-2 w-48 animate-pulse rounded dark:bg-gray-700"></div>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-3 w-40 animate-pulse rounded dark:bg-gray-700"></div>
</div>
<div className="flex items-center gap-2">
<div className="bg-muted h-3 w-16 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
</div>
</div>
)}
</div>
))}
<div className="border-border space-y-2 rounded-lg border-2 border-dashed p-3">
<div className="bg-muted h-12 animate-pulse rounded dark:bg-gray-800"></div>
<div className="bg-muted h-7 w-16 animate-pulse rounded dark:bg-gray-800"></div>
</div>
<div className="flex gap-2 pt-3">
<div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
<div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
</div>
</CardContent>
<LoadingActionsCardContent />
</Card>
);
}
@ -143,7 +151,7 @@ export function ThreadViewLoading({ onBackToHome }: ThreadViewLoadingProps) {
<div className="flex h-full flex-1 flex-col">
<div className="flex-1 space-y-4 overflow-y-auto p-4">
<LoadingActionsSection />
<LoadingActionsCard />
</div>
</div>
</div>