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/core": "^0.3.56",
"@langchain/google-genai": "^0.2.9", "@langchain/google-genai": "^0.2.9",
"@langchain/langgraph": "^0.3.3", "@langchain/langgraph": "^0.3.3",
"@langchain/langgraph-sdk": "^0.0.84", "@langchain/langgraph-sdk": "^0.0.85",
"@langchain/openai": "^0.5.10", "@langchain/openai": "^0.5.10",
"@octokit/rest": "^22.0.0", "@octokit/rest": "^22.0.0",
"@open-swe/shared": "*", "@open-swe/shared": "*",

View file

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

View file

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

View file

@ -6,9 +6,9 @@ export function RepositoryBranchSelectors() {
const [threadId] = useQueryState("threadId"); const [threadId] = useQueryState("threadId");
const chatStarted = !!threadId; const chatStarted = !!threadId;
const defaultButtonStyles = 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 = 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 ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View file

@ -155,13 +155,13 @@ export function TerminalInput({
<Button <Button
onClick={handleSend} onClick={handleSend}
disabled={disabled || !message.trim() || !selectedRepository} disabled={disabled || !message.trim() || !selectedRepository}
size="sm" size="icon"
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" variant="brand"
> >
{loading ? ( {loading ? (
<Loader2 className="size-3 animate-spin" /> <Loader2 className="size-4 animate-spin" />
) : ( ) : (
<Send className="size-3" /> <Send className="size-4" />
)} )}
</Button> </Button>
</div> </div>

View file

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

View file

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

View file

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