Staging june24 (#319)

* empty state programmer planner tabs

* add scroll: on pageload, buttonclick, tabswitch

* format

* init step dark mode style

* format

* submit button styling

* improve to use brand variant

* increase to 8x8px and change padding to adjust btn pos

* minor format

* fix light theme repo hover styling

* bump deps

* fix input button styling

* fix styling for scroll to bottom

---------

Co-authored-by: starmorph <dylan@starmorph.com>
Co-authored-by: Dylan Boudro <121908331+starmorph@users.noreply.github.com>
This commit is contained in:
Brace Sproul 2025-06-26 15:08:16 -06:00 • committed by GitHub
parent 64d34aa7bb
commit 9d7a74a976
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 243 additions and 118 deletions

View file

@ -27,7 +27,7 @@
"@langchain/core": "^0.3.56",
"@langchain/google-genai": "^0.2.9",
"@langchain/langgraph": "^0.3.3",
"@langchain/langgraph-sdk": "^0.0.84",
"@langchain/langgraph-sdk": "^0.0.85",
"@langchain/openai": "^0.5.10",
"@octokit/rest": "^22.0.0",
"@open-swe/shared": "*",

View file

@ -21,7 +21,7 @@
"dependencies": {
"@langchain/core": "^0.3.57",
"@langchain/langgraph": "^0.3.3",
"@langchain/langgraph-sdk": "^0.0.83",
"@langchain/langgraph-sdk": "^0.0.85",
"@open-swe/shared": "*",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-avatar": "^1.1.3",

View file

@ -41,9 +41,15 @@ export function InitializeStep({
const stepStatusIcon = {
waiting: (
<div className={cn("h-3.5 w-3.5 rounded-full border border-gray-300")} />
<div
className={cn(
"h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-600",
)}
/>
),
generating: (
<Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500 dark:text-gray-400" />
),
generating: <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />,
success: <CheckCircle className="h-3.5 w-3.5 text-green-500" />,
error: <XCircle className="h-3.5 w-3.5 text-red-500" />,
};
@ -53,11 +59,15 @@ export function InitializeStep({
case "loading":
return (
<div
className={cn("h-3.5 w-3.5 rounded-full border border-gray-300")}
className={cn(
"h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-600",
)}
/>
);
case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500" />;
return (
<Loader2 className="h-3.5 w-3.5 animate-spin text-gray-500 dark:text-gray-400" />
);
case "done":
return success ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
@ -79,11 +89,11 @@ export function InitializeStep({
};
return (
<div className="overflow-hidden rounded-md border border-gray-200">
<div className="overflow-hidden rounded-md border border-gray-200 dark:border-gray-700">
{/* Collapse/Expand Icon */}
<div className="relative flex items-center border-b border-gray-200 bg-gray-50 p-2">
<GitBranch className="mr-2 h-3.5 w-3.5 text-gray-500" />
<span className="flex-1 text-xs font-normal text-gray-800">
<div className="relative flex items-center border-b border-gray-200 bg-gray-50 p-2 dark:border-gray-700 dark:bg-gray-800">
<GitBranch className="mr-2 h-3.5 w-3.5 text-gray-500 dark:text-gray-400" />
<span className="flex-1 text-xs font-normal text-gray-800 dark:text-gray-200">
{getStatusText()}
</span>
{getStatusIcon()}
@ -118,7 +128,7 @@ export function InitializeStep({
] ?? (
<div
className={cn(
"h-3.5 w-3.5 rounded-full border border-gray-300",
"h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-600",
)}
/>
)}
@ -128,7 +138,7 @@ export function InitializeStep({
"font-normal",
step.status === "error"
? "text-red-500"
: "text-gray-800",
: "text-gray-800 dark:text-gray-200",
)}
>
{step.name}

View file

@ -6,9 +6,9 @@ export function RepositoryBranchSelectors() {
const [threadId] = useQueryState("threadId");
const chatStarted = !!threadId;
const defaultButtonStyles =
"bg-inherit border-none text-gray-500 hover:text-gray-300 text-xs p-0 h-fit hover:bg-inherit";
"bg-inherit border-none text-gray-500 hover:text-black dark:hover:text-gray-300 text-xs p-0 h-fit hover:bg-inherit";
const defaultStylesChatStarted =
"hover:bg-inherit cursor-default hover:cursor-default hover:text-gray-300 hover:border-gray-300 hover:ring-inherit";
"hover:bg-inherit cursor-default hover:cursor-default hover:text-black dark:hover:text-gray-300 hover:border-gray-300 hover:ring-inherit";
return (
<div className="flex items-center gap-2">

View file

@ -155,13 +155,13 @@ export function TerminalInput({
<Button
onClick={handleSend}
disabled={disabled || !message.trim() || !selectedRepository}
size="sm"
className="bg-muted-foreground/20 hover:bg-muted-foreground/30 h-7 w-7 self-end p-0 dark:bg-gray-700 hover:dark:bg-gray-600"
size="icon"
variant="brand"
>
{loading ? (
<Loader2 className="size-3 animate-spin" />
<Loader2 className="size-4 animate-spin" />
) : (
<Send className="size-3" />
<Send className="size-4" />
)}
</Button>
</div>

View file

@ -1,14 +1,15 @@
"use client";
import { v4 as uuidv4 } from "uuid";
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import {
ArrowLeft,
GitBranch,
Send,
Terminal,
Clock,
User,
Bot,
Copy,
@ -23,10 +24,16 @@ import { ThreadDisplayInfo } from "./types";
import { useStream } from "@langchain/langgraph-sdk/react";
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { ActionsRenderer } from "./actions-renderer";
import { ThemeToggle } from "../theme-toggle";
import { HumanMessage } from "@langchain/core/messages";
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
import { StickToBottom } from "use-stick-to-bottom";
import {
StickyToBottomContent,
ScrollToBottom,
} from "../../utils/scroll-utils";
const PROGRAMMER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PROGRAMMER_ASSISTANT_ID;
const PLANNER_ASSISTANT_ID = process.env.NEXT_PUBLIC_PLANNER_ASSISTANT_ID;
@ -100,9 +107,23 @@ export function ThreadView({
const [programmerSession, setProgrammerSession] =
useState<ManagerGraphState["programmerSession"]>();
if (!stream.messages?.length) {
return null;
}
const prevMessageLength = useRef(0);
useEffect(() => {
if (
stream.messages &&
stream.messages.length !== prevMessageLength.current
) {
prevMessageLength.current = stream.messages.length;
}
}, [stream.messages]);
useEffect(() => {
// Auto-scroll to bottom when tab switches (requirement fulfilled)
// StickToBottom handles the actual scrolling via scrollToBottom()
}, [selectedTab]);
// Don't early return for empty messages - show loading/empty state instead
// This prevents blank pages when navigating to existing threads
const handleSendMessage = () => {
if (chatInput.trim()) {
@ -179,40 +200,58 @@ export function ThreadView({
{/* Left Side - Chat Interface */}
<div className="border-border bg-muted/30 flex h-full w-1/3 flex-col border-r dark:bg-gray-950">
{/* Chat Messages */}
<div className="flex-1 space-y-4 overflow-y-auto p-4">
{filteredMessages.map((message) => (
<div
key={message.id}
className="group flex gap-3"
>
<div className="flex-shrink-0">
{message.type === "human" ? (
<div className="bg-muted flex h-6 w-6 items-center justify-center rounded-full dark:bg-gray-700">
<User className="text-muted-foreground h-3 w-3" />
</div>
) : (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Bot className="h-3 w-3 text-blue-700 dark:text-blue-400" />
</div>
)}
</div>
<div className="relative flex-1 space-y-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs font-medium">
{message.type === "human" ? "You" : "Agent"}
</span>
<div className="relative flex-1">
<StickToBottom
className="absolute inset-0"
initial={false}
>
<StickyToBottomContent
className="h-full overflow-y-auto"
contentClassName="space-y-4 p-4"
content={
<>
{filteredMessages.map((message) => (
<div
key={message.id}
className="group flex gap-3"
>
<div className="flex-shrink-0">
{message.type === "human" ? (
<div className="bg-muted flex h-6 w-6 items-center justify-center rounded-full dark:bg-gray-700">
<User className="text-muted-foreground h-3 w-3" />
</div>
) : (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
<Bot className="h-3 w-3 text-blue-700 dark:text-blue-400" />
</div>
)}
</div>
<div className="relative flex-1 space-y-1">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs font-medium">
{message.type === "human" ? "You" : "Agent"}
</span>
</div>
<div className="text-foreground text-sm leading-relaxed">
{getMessageContentString(message.content)}
</div>
<div className="absolute right-0 -bottom-5 opacity-0 transition-opacity group-hover:opacity-100">
<MessageCopyButton
content={getMessageContentString(message.content)}
/>
</div>
</div>
</div>
))}
</>
}
footer={
<div className="absolute right-0 left-0 bottom-4 flex justify-center w-full">
<ScrollToBottom className="animate-in fade-in-0 zoom-in-95" />
</div>
<div className="text-foreground text-sm leading-relaxed">
{getMessageContentString(message.content)}
</div>
<div className="absolute right-0 -bottom-5 opacity-0 transition-opacity group-hover:opacity-100">
<MessageCopyButton
content={getMessageContentString(message.content)}
/>
</div>
</div>
</div>
))}
}
/>
</StickToBottom>
</div>
{/* Chat Input - Fixed at bottom */}
@ -233,10 +272,10 @@ export function ThreadView({
<Button
onClick={handleSendMessage}
disabled={!chatInput.trim()}
size="sm"
className="bg-muted hover:bg-muted/80 h-10 w-10 self-end p-0 dark:bg-gray-700 hover:dark:bg-gray-600"
size="icon"
variant="brand"
>
<Send className="h-4 w-4" />
<Send className="size-4" />
</Button>
</div>
<div className="text-muted-foreground mt-2 text-xs">
@ -247,56 +286,87 @@ export function ThreadView({
{/* Right Side - Actions & Plan */}
<div className="flex h-full flex-1 flex-col">
<div className="flex-1 space-y-4 overflow-y-auto p-4">
<Tabs
defaultValue="planner"
className="w-full"
value={selectedTab}
onValueChange={(value) =>
setSelectedTab(value as "planner" | "programmer")
}
<div className="relative flex-1">
<StickToBottom
className="absolute inset-0"
initial={true}
>
<TabsList className="bg-muted/70 dark:bg-gray-800">
<TabsTrigger value="planner">Planner</TabsTrigger>
<TabsTrigger value="programmer">Programmer</TabsTrigger>
</TabsList>
<TabsContent value="planner">
<Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardContent className="space-y-2 p-3 pt-0">
{plannerThreadId &&
plannerRunId &&
PLANNER_ASSISTANT_ID && (
<ActionsRenderer<PlannerGraphState>
graphId={PLANNER_ASSISTANT_ID}
threadId={plannerThreadId}
runId={plannerRunId}
setProgrammerSession={setProgrammerSession}
programmerSession={programmerSession}
setSelectedTab={setSelectedTab}
/>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="programmer">
<Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardContent className="space-y-2 p-3 pt-0">
{programmerSession && PROGRAMMER_ASSISTANT_ID && (
<ActionsRenderer<PlannerGraphState>
graphId={PROGRAMMER_ASSISTANT_ID}
threadId={programmerSession.threadId}
runId={programmerSession.runId}
/>
)}
{!programmerSession && (
<div className="text-muted-foreground text-xs">
No programmer session
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
<StickyToBottomContent
className="h-full overflow-y-auto"
contentClassName="space-y-4 p-4"
content={
<Tabs
defaultValue="planner"
className="w-full"
value={selectedTab}
onValueChange={(value) =>
setSelectedTab(value as "planner" | "programmer")
}
>
<TabsList className="bg-muted/70 dark:bg-gray-800">
<TabsTrigger value="planner">Planner</TabsTrigger>
<TabsTrigger value="programmer">Programmer</TabsTrigger>
</TabsList>
<TabsContent value="planner">
<Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardContent className="space-y-2 p-3 pt-0">
{plannerThreadId &&
plannerRunId &&
PLANNER_ASSISTANT_ID && (
<ActionsRenderer<PlannerGraphState>
graphId={PLANNER_ASSISTANT_ID}
threadId={plannerThreadId}
runId={plannerRunId}
setProgrammerSession={setProgrammerSession}
programmerSession={programmerSession}
setSelectedTab={setSelectedTab}
/>
)}
{!(
plannerThreadId &&
plannerRunId &&
PLANNER_ASSISTANT_ID
) && (
<div className="flex items-center justify-center gap-2 py-8">
<Clock className="text-muted-foreground size-4" />
<span className="text-muted-foreground text-sm">
No planner session
</span>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="programmer">
<Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardContent className="space-y-2 p-3 pt-0">
{programmerSession && PROGRAMMER_ASSISTANT_ID && (
<ActionsRenderer<GraphState>
graphId={PROGRAMMER_ASSISTANT_ID}
threadId={programmerSession.threadId}
runId={programmerSession.runId}
/>
)}
{!programmerSession && (
<div className="flex items-center justify-center gap-2 py-8">
<Terminal className="text-muted-foreground size-4" />
<span className="text-muted-foreground text-sm">
No programmer session
</span>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
}
footer={
<div className="absolute right-0 left-0 bottom-4 flex justify-center w-full">
<ScrollToBottom className="animate-in fade-in-0 zoom-in-95" />
</div>
}
/>
</StickToBottom>
</div>
</div>
</div>

View file

@ -0,0 +1,45 @@
import { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { ArrowDown } from "lucide-react";
import { useStickToBottomContext } from "use-stick-to-bottom";
export function StickyToBottomContent(props: {
content: ReactNode;
footer?: ReactNode;
className?: string;
contentClassName?: string;
}) {
const context = useStickToBottomContext();
return (
<div
ref={context.scrollRef}
style={{ width: "100%", height: "100%" }}
className={props.className}
>
<div
ref={context.contentRef}
className={props.contentClassName}
>
{props.content}
</div>
{props.footer}
</div>
);
}
export function ScrollToBottom(props: { className?: string }) {
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
if (isAtBottom) return null;
return (
<Button
variant="outline"
size="sm"
className={props.className}
onClick={() => scrollToBottom()}
>
<ArrowDown className="h-4 w-4" />
<span>Scroll to bottom</span>
</Button>
);
}

View file

@ -20,7 +20,7 @@
"typescript": "^5"
},
"resolutions": {
"@langchain/langgraph-sdk": "^0.0.83",
"@langchain/langgraph-sdk": "^0.0.85",
"@langchain/core": "^0.3.58"
},
"packageManager": "yarn@3.5.1"

View file

@ -19,7 +19,7 @@
"dependencies": {
"@langchain/core": "^0.3.56",
"@langchain/langgraph": "^0.3.3",
"@langchain/langgraph-sdk": "^0.0.83",
"@langchain/langgraph-sdk": "^0.0.85",
"@octokit/rest": "^22.0.0",
"zod": "^3.25.32"
},

View file

@ -1803,9 +1803,9 @@ __metadata:
languageName: node
linkType: hard
"@langchain/langgraph-sdk@npm:^0.0.83":
version: 0.0.83
resolution: "@langchain/langgraph-sdk@npm:0.0.83"
"@langchain/langgraph-sdk@npm:^0.0.85":
version: 0.0.85
resolution: "@langchain/langgraph-sdk@npm:0.0.85"
dependencies:
"@types/json-schema": ^7.0.15
p-queue: ^6.6.2
@ -1819,7 +1819,7 @@ __metadata:
optional: true
react:
optional: true
checksum: c6d037b8b9486456a8180d892e8b05ad3c2332456d7d237597720ebe42dd6b126f1aed963a0d2a681405724b7b8e0eac0607994b8e527d2f0e756e4c9e30c19e
checksum: f6d206c718cf4cb5d69dd677f2afa38325fdd69b485310a6beb61f06f24b2b4a8c0446eacbe7911c02cedc8bd4b719e22894c6f9b93572e6665c6a609cc9f511
languageName: node
linkType: hard
@ -2264,7 +2264,7 @@ __metadata:
"@langchain/google-genai": ^0.2.9
"@langchain/langgraph": ^0.3.3
"@langchain/langgraph-cli": latest
"@langchain/langgraph-sdk": ^0.0.84
"@langchain/langgraph-sdk": ^0.0.85
"@langchain/openai": ^0.5.10
"@octokit/rest": ^22.0.0
"@open-swe/shared": "*"
@ -2299,7 +2299,7 @@ __metadata:
"@eslint/js": ^9.19.0
"@langchain/core": ^0.3.56
"@langchain/langgraph": ^0.3.3
"@langchain/langgraph-sdk": ^0.0.83
"@langchain/langgraph-sdk": ^0.0.85
"@octokit/rest": ^22.0.0
"@octokit/types": ^12.0.0
"@tsconfig/recommended": ^1.0.8
@ -2324,7 +2324,7 @@ __metadata:
"@eslint/js": ^9.19.0
"@langchain/core": ^0.3.57
"@langchain/langgraph": ^0.3.3
"@langchain/langgraph-sdk": ^0.0.83
"@langchain/langgraph-sdk": ^0.0.85
"@open-swe/shared": "*"
"@radix-ui/react-alert-dialog": ^1.1.14
"@radix-ui/react-avatar": ^1.1.3