feat: Dark mode (#241)

* feat: Dark mode

* more work

* more dark/light mode, refactor thread card

* fix rest of dark mode

* cr

* cr

* cr
This commit is contained in:
Brace Sproul 2025-06-18 16:44:41 -07:00 • committed by GitHub
parent b752b2326e
commit de8d465695
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 504 additions and 675 deletions

View file

@ -29,7 +29,9 @@ export default function ThreadPage({
reconnectOnMount: true, reconnectOnMount: true,
}); });
const { threads, threadsLoading } = useThreads<GraphState>(); const { threads, threadsLoading } = useThreads<GraphState>(
process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID,
);
// Find the thread by ID // Find the thread by ID
const thread = threads?.find((t) => t.thread_id === thread_id); const thread = threads?.find((t) => t.thread_id === thread_id);
@ -37,7 +39,7 @@ export default function ThreadPage({
router.push("/chat"); router.push("/chat");
}; };
if (!thread || threadsLoading || !threadsLoading) { if (!thread || threadsLoading) {
return <ThreadViewLoading onBackToHome={handleBackToHome} />; return <ThreadViewLoading onBackToHome={handleBackToHome} />;
} }
@ -46,17 +48,12 @@ export default function ThreadPage({
threads?.map(threadToDisplayInfo) ?? []; threads?.map(threadToDisplayInfo) ?? [];
const currentDisplayThread = threadToDisplayInfo(thread); const currentDisplayThread = threadToDisplayInfo(thread);
const handleThreadSelect = (selectedThread: ThreadDisplayInfo) => {
router.push(`/chat/${selectedThread.id}`);
};
return ( return (
<div className="h-screen bg-black"> <div className="bg-background fixed inset-0">
<ThreadView <ThreadView
stream={stream} stream={stream}
displayThread={currentDisplayThread} displayThread={currentDisplayThread}
allDisplayThreads={displayThreads} allDisplayThreads={displayThreads}
onThreadSelect={handleThreadSelect}
onBackToHome={handleBackToHome} onBackToHome={handleBackToHome}
/> />
</div> </div>

View file

@ -18,7 +18,7 @@ export default function ChatPage() {
threads?.map(threadToDisplayInfo) ?? []; threads?.map(threadToDisplayInfo) ?? [];
return ( return (
<div className="h-screen overflow-y-hidden bg-black"> <div className="bg-background h-screen overflow-y-hidden">
<Suspense> <Suspense>
<Toaster /> <Toaster />
<GitHubAppProvider> <GitHubAppProvider>

View file

@ -3,32 +3,23 @@
import type React from "react"; import type React from "react";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import { ArrowLeft, Search, Filter } from "lucide-react";
ArrowLeft,
Search,
Filter,
CheckCircle,
XCircle,
Loader2,
GitBranch,
GitPullRequest,
Bug,
Calendar,
Clock,
} from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types"; import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types";
import { useThreads } from "@/hooks/useThreads"; import { useThreads } from "@/hooks/useThreads";
import { GraphState } from "@open-swe/shared/open-swe/types"; import { GraphState } from "@open-swe/shared/open-swe/types";
import { ThreadCard } from "@/components/v2/thread-card";
import { ThemeToggle } from "@/components/theme-toggle";
type FilterStatus = "all" | "running" | "completed" | "failed" | "pending"; type FilterStatus = "all" | "running" | "completed" | "failed" | "pending";
export default function AllThreadsPage() { export default function AllThreadsPage() {
const router = useRouter(); const router = useRouter();
const { threads } = useThreads<GraphState>(); const { threads } = useThreads<GraphState>(
process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID,
);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<FilterStatus>("all"); const [statusFilter, setStatusFilter] = useState<FilterStatus>("all");
@ -36,51 +27,6 @@ export default function AllThreadsPage() {
const displayThreads: ThreadDisplayInfo[] = const displayThreads: ThreadDisplayInfo[] =
threads?.map(threadToDisplayInfo) ?? []; threads?.map(threadToDisplayInfo) ?? [];
const getStatusColor = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return "bg-blue-950 text-blue-400";
case "completed":
return "bg-green-950 text-green-400";
case "failed":
return "bg-red-950 text-red-400";
case "pending":
return "bg-yellow-950 text-yellow-400";
default:
return "bg-gray-800 text-gray-400";
}
};
const getStatusIcon = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return <Loader2 className="h-4 w-4 animate-spin" />;
case "completed":
return <CheckCircle className="h-4 w-4" />;
case "failed":
return <XCircle className="h-4 w-4" />;
case "pending":
return <Clock className="h-4 w-4" />;
default:
return null;
}
};
const getPRStatusColor = (status: string) => {
switch (status) {
case "merged":
return "text-purple-400";
case "open":
return "text-green-400";
case "draft":
return "text-gray-400";
case "closed":
return "text-red-400";
default:
return "text-gray-400";
}
};
// Filter and search threads // Filter and search threads
const filteredThreads = displayThreads.filter((thread) => { const filteredThreads = displayThreads.filter((thread) => {
const matchesSearch = const matchesSearch =
@ -107,50 +53,51 @@ export default function AllThreadsPage() {
pending: displayThreads.filter((t) => t.status === "pending").length, pending: displayThreads.filter((t) => t.status === "pending").length,
}; };
const handleThreadClick = (thread: ThreadDisplayInfo) => {
router.push(`/chat/${thread.id}`);
};
return ( return (
<div className="flex h-screen flex-col bg-black"> <div className="bg-background flex h-screen flex-col">
{/* Header */} {/* Header */}
<div className="border-b border-gray-900 bg-black px-4 py-3"> <div className="border-border bg-card border-b px-4 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 text-gray-600 hover:bg-gray-900 hover:text-gray-400" className="text-muted-foreground hover:bg-muted hover:text-foreground h-6 w-6 p-0"
onClick={() => router.push("/chat")} onClick={() => router.push("/chat")}
> >
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
</Button> </Button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-500"></div> <div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="font-mono text-sm text-gray-400">All Threads</span> <span className="text-muted-foreground font-mono text-sm">
</div> All Threads
<div className="ml-auto flex items-center gap-2">
<span className="text-xs text-gray-600">
{filteredThreads.length} threads
</span> </span>
</div> </div>
<div className="ml-auto flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
{filteredThreads.length} threads
</span>
</div>
<ThemeToggle />
</div>
</div> </div>
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="border-b border-gray-900 bg-gray-950 px-4 py-3"> <div className="border-border bg-muted/50 border-b px-4 py-3 dark:bg-gray-950">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="relative max-w-md flex-1"> <div className="relative max-w-md flex-1">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-500" /> <Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input <Input
placeholder="Search threads..." placeholder="Search threads..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="border-gray-700 bg-gray-900 pl-10 text-gray-300 placeholder:text-gray-600" className="border-border bg-background text-foreground placeholder:text-muted-foreground pl-10 dark:bg-gray-900"
/> />
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Filter className="h-4 w-4 text-gray-500" /> <Filter className="text-muted-foreground h-4 w-4" />
<span className="mr-2 text-xs text-gray-500">Filter:</span> <span className="text-muted-foreground mr-2 text-xs">Filter:</span>
{( {(
[ [
"all", "all",
@ -166,8 +113,8 @@ export default function AllThreadsPage() {
size="sm" size="sm"
className={`h-7 text-xs ${ className={`h-7 text-xs ${
statusFilter === status statusFilter === status
? "bg-gray-700 text-gray-200" ? "bg-muted text-foreground dark:bg-gray-700"
: "text-gray-500 hover:bg-gray-800 hover:text-gray-300" : "text-muted-foreground hover:bg-muted hover:text-foreground"
}`} }`}
onClick={() => setStatusFilter(status)} onClick={() => setStatusFilter(status)}
> >
@ -176,7 +123,7 @@ export default function AllThreadsPage() {
: status.charAt(0).toUpperCase() + status.slice(1)} : status.charAt(0).toUpperCase() + status.slice(1)}
<Badge <Badge
variant="secondary" variant="secondary"
className="ml-1 bg-gray-800 text-xs text-gray-400" className="bg-muted/70 text-muted-foreground ml-1 text-xs dark:bg-gray-800"
> >
{statusCounts[status]} {statusCounts[status]}
</Badge> </Badge>
@ -197,12 +144,12 @@ export default function AllThreadsPage() {
return ( return (
<div key={status}> <div key={status}>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<h2 className="text-base font-semibold text-gray-300 capitalize"> <h2 className="text-foreground text-base font-semibold capitalize">
{status} Threads {status} Threads
</h2> </h2>
<Badge <Badge
variant="secondary" variant="secondary"
className="bg-gray-800 text-xs text-gray-400" className="bg-muted/70 text-muted-foreground text-xs dark:bg-gray-800"
> >
{threads.length} {threads.length}
</Badge> </Badge>
@ -212,10 +159,6 @@ export default function AllThreadsPage() {
<ThreadCard <ThreadCard
key={thread.id} key={thread.id}
thread={thread} thread={thread}
onClick={() => handleThreadClick(thread)}
getStatusColor={getStatusColor}
getStatusIcon={getStatusIcon}
getPRStatusColor={getPRStatusColor}
/> />
))} ))}
</div> </div>
@ -230,10 +173,6 @@ export default function AllThreadsPage() {
<ThreadCard <ThreadCard
key={thread.id} key={thread.id}
thread={thread} thread={thread}
onClick={() => handleThreadClick(thread)}
getStatusColor={getStatusColor}
getStatusIcon={getStatusIcon}
getPRStatusColor={getPRStatusColor}
/> />
))} ))}
</div> </div>
@ -241,8 +180,8 @@ export default function AllThreadsPage() {
{filteredThreads.length === 0 && ( {filteredThreads.length === 0 && (
<div className="py-12 text-center"> <div className="py-12 text-center">
<div className="mb-2 text-gray-500">No threads found</div> <div className="text-muted-foreground mb-2">No threads found</div>
<div className="text-xs text-gray-600"> <div className="text-muted-foreground/70 text-xs">
{searchQuery {searchQuery
? "Try adjusting your search query" ? "Try adjusting your search query"
: "No threads match the selected filter"} : "No threads match the selected filter"}
@ -254,95 +193,3 @@ export default function AllThreadsPage() {
</div> </div>
); );
} }
interface ThreadCardProps {
thread: ThreadDisplayInfo;
onClick: () => void;
getStatusColor: (status: ThreadDisplayInfo["status"]) => string;
getStatusIcon: (status: ThreadDisplayInfo["status"]) => React.ReactNode;
getPRStatusColor: (status: string) => string;
}
function ThreadCard({
thread,
onClick,
getStatusColor,
getStatusIcon,
getPRStatusColor,
}: ThreadCardProps) {
return (
<Card
className="cursor-pointer border-gray-800 bg-gray-950 transition-shadow hover:bg-gray-900 hover:shadow-lg"
onClick={onClick}
>
<CardHeader className="p-3 pb-2">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="truncate text-sm font-medium text-gray-300">
{thread.title}
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="h-2 w-2 text-gray-600" />
<span className="truncate text-xs text-gray-500">
{thread.repository}
</span>
</div>
</div>
<Badge
variant="secondary"
className={`${getStatusColor(thread.status)} text-xs`}
>
<div className="flex items-center gap-1">
{getStatusIcon(thread.status)}
<span className="capitalize">{thread.status}</span>
</div>
</Badge>
</div>
</CardHeader>
<CardContent className="p-3 pt-0">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600">
{thread.taskCount} tasks
</span>
<span className="text-xs text-gray-600">•</span>
<div className="flex items-center gap-1">
<Calendar className="h-2 w-2 text-gray-600" />
<span className="text-xs text-gray-600">
{thread.lastActivity}
</span>
</div>
</div>
<div className="flex items-center gap-1">
{thread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-gray-500 hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
window.open(thread.githubIssue!.url, "_blank");
}}
>
<Bug className="h-3 w-3" />
</Button>
)}
{thread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={`h-5 w-5 p-0 hover:text-gray-300 ${getPRStatusColor(thread.pullRequest.status)}`}
onClick={(e) => {
e.stopPropagation();
window.open(thread.pullRequest!.url, "_blank");
}}
>
<GitPullRequest className="h-3 w-3" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -3,6 +3,7 @@ import "./globals.css";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import React from "react"; import React from "react";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { ThemeProvider } from "@/components/theme-provider";
const inter = Inter({ const inter = Inter({
subsets: ["latin"], subsets: ["latin"],
@ -26,9 +27,17 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html
lang="en"
suppressHydrationWarning
>
<body className={inter.className}> <body className={inter.className}>
<NuqsAdapter>{children}</NuqsAdapter> <ThemeProvider
defaultTheme="system"
storageKey="theme"
>
<NuqsAdapter>{children}</NuqsAdapter>
</ThemeProvider>
</body> </body>
</html> </html>
); );

View file

@ -54,10 +54,12 @@ export function ActionStep(props: ActionStepProps) {
switch (props.status) { switch (props.status) {
case "loading": case "loading":
return ( return (
<div className="h-3.5 w-3.5 rounded-full border border-gray-600" /> <div className="border-border h-3.5 w-3.5 rounded-full border" />
); );
case "generating": case "generating":
return <Loader2 className="h-3.5 w-3.5 animate-spin text-gray-400" />; return (
<Loader2 className="text-muted-foreground h-3.5 w-3.5 animate-spin" />
);
case "done": case "done":
return props.success ? ( return props.success ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500" /> <CheckCircle className="h-3.5 w-3.5 text-green-500" />
@ -106,13 +108,13 @@ export function ActionStep(props: ActionStepProps) {
const renderHeaderIcon = () => { const renderHeaderIcon = () => {
if (props.status === "loading" || !("actionType" in props)) { if (props.status === "loading" || !("actionType" in props)) {
// In loading state, we don't know the type yet, use a generic icon // In loading state, we don't know the type yet, use a generic icon
return <Loader2 className="mr-2 h-3.5 w-3.5 text-gray-400" />; return <Loader2 className="text-muted-foreground mr-2 h-3.5 w-3.5" />;
} }
return props.actionType === "shell" ? ( return props.actionType === "shell" ? (
<Terminal className="mr-2 h-3.5 w-3.5 text-gray-400" /> <Terminal className="text-muted-foreground mr-2 h-3.5 w-3.5" />
) : ( ) : (
<FileCode className="mr-2 h-3.5 w-3.5 text-gray-400" /> <FileCode className="text-muted-foreground mr-2 h-3.5 w-3.5" />
); );
}; };
@ -120,7 +122,7 @@ export function ActionStep(props: ActionStepProps) {
const renderHeaderContent = () => { const renderHeaderContent = () => {
if (props.status === "loading" || !("actionType" in props)) { if (props.status === "loading" || !("actionType" in props)) {
return ( return (
<span className="text-xs font-normal text-gray-300"> <span className="text-foreground/80 text-xs font-normal">
Preparing action... Preparing action...
</span> </span>
); );
@ -130,18 +132,18 @@ export function ActionStep(props: ActionStepProps) {
return ( return (
<div className="flex-1"> <div className="flex-1">
{props.workdir && ( {props.workdir && (
<div className="mb-0.5 text-xs font-normal text-gray-400"> <div className="text-muted-foreground mb-0.5 text-xs font-normal">
{props.workdir} {props.workdir}
</div> </div>
)} )}
<code className="text-xs font-normal text-gray-300"> <code className="text-foreground/80 text-xs font-normal">
{props.command.join(" ")} {props.command.join(" ")}
</code> </code>
</div> </div>
); );
} else { } else {
return ( return (
<code className="flex-1 text-xs font-normal text-gray-300"> <code className="text-foreground/80 flex-1 text-xs font-normal">
{props.file} {props.file}
</code> </code>
); );
@ -156,12 +158,12 @@ export function ActionStep(props: ActionStepProps) {
if (props.actionType === "shell" && props.output) { if (props.actionType === "shell" && props.output) {
return ( return (
<div className="overflow-x-auto bg-gray-900 p-2 text-gray-200"> <div className="bg-muted text-foreground/90 overflow-x-auto p-2 dark:bg-gray-900">
<pre className="text-xs font-normal whitespace-pre-wrap"> <pre className="text-xs font-normal whitespace-pre-wrap">
{props.output} {props.output}
</pre> </pre>
{props.errorCode !== undefined && !props.success && ( {props.errorCode !== undefined && !props.success && (
<div className="mt-1 text-xs text-red-400"> <div className="mt-1 text-xs text-red-500 dark:text-red-400">
Exit code: {props.errorCode} Exit code: {props.errorCode}
</div> </div>
)} )}
@ -169,23 +171,25 @@ export function ActionStep(props: ActionStepProps) {
); );
} else if (props.actionType === "apply-patch" && props.diff) { } else if (props.actionType === "apply-patch" && props.diff) {
return ( return (
<div className="overflow-x-auto bg-gray-900 p-2"> <div className="bg-muted overflow-x-auto p-2 dark:bg-gray-900">
<pre <pre
className="text-xs font-normal whitespace-pre-wrap text-gray-200" className="text-foreground/90 text-xs font-normal whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: formatDiff(props.diff) }} dangerouslySetInnerHTML={{ __html: formatDiff(props.diff) }}
/> />
{!props.success && props.errorMessage && ( {!props.success && props.errorMessage && (
<div className="mt-2 rounded border border-red-700/30 bg-red-900/30 p-2 text-xs text-red-400"> <div className="mt-2 rounded border border-red-700/30 bg-red-100/30 p-2 text-xs text-red-600 dark:bg-red-900/30 dark:text-red-400">
{props.errorMessage} {props.errorMessage}
</div> </div>
)} )}
{!props.success && props.fixedDiff && ( {!props.success && props.fixedDiff && (
<div className="mt-2"> <div className="mt-2">
<div className="mb-1 text-xs text-gray-400">Fixed diff:</div> <div className="text-muted-foreground mb-1 text-xs">
Fixed diff:
</div>
<pre <pre
className="text-xs font-normal whitespace-pre-wrap text-gray-200" className="text-foreground/90 text-xs font-normal whitespace-pre-wrap"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: formatDiff(props.fixedDiff), __html: formatDiff(props.fixedDiff),
}} }}
@ -200,36 +204,36 @@ export function ActionStep(props: ActionStepProps) {
}; };
return ( return (
<div className="overflow-hidden rounded-md border border-gray-700"> <div className="border-border overflow-hidden rounded-md border">
{props.reasoningText && ( {props.reasoningText && (
<div className="border-b border-blue-800 bg-blue-900/50 p-2"> <div className="border-b border-blue-300 bg-blue-100/50 p-2 dark:border-blue-800 dark:bg-blue-900/50">
<button <button
onClick={() => setShowReasoning(!showReasoning)} onClick={() => setShowReasoning(!showReasoning)}
className="flex items-center gap-1 text-xs font-normal text-blue-400 hover:text-blue-300" className="flex items-center gap-1 text-xs font-normal text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
> >
<MessageSquare className="h-3 w-3" /> <MessageSquare className="h-3 w-3" />
{showReasoning ? "Hide reasoning" : "Show reasoning"} {showReasoning ? "Hide reasoning" : "Show reasoning"}
</button> </button>
{showReasoning && ( {showReasoning && (
<p className="mt-1 text-xs font-normal text-blue-300"> <p className="mt-1 text-xs font-normal text-blue-700 dark:text-blue-300">
{props.reasoningText} {props.reasoningText}
</p> </p>
)} )}
</div> </div>
)} )}
<div className="flex items-center border-b border-gray-700 bg-gray-800 p-2"> <div className="border-border bg-card flex items-center border-b p-2 dark:bg-gray-800">
{renderHeaderIcon()} {renderHeaderIcon()}
{renderHeaderContent()} {renderHeaderContent()}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-normal text-gray-400"> <span className="text-muted-foreground text-xs font-normal">
{getStatusText()} {getStatusText()}
</span> </span>
{getStatusIcon()} {getStatusIcon()}
{shouldShowToggle() && ( {shouldShowToggle() && (
<button <button
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
className="text-gray-400 hover:text-gray-300" className="text-muted-foreground hover:text-foreground"
> >
{expanded ? ( {expanded ? (
<ChevronUp className="h-3.5 w-3.5" /> <ChevronUp className="h-3.5 w-3.5" />
@ -244,16 +248,16 @@ export function ActionStep(props: ActionStepProps) {
{renderContent()} {renderContent()}
{props.summaryText && props.status === "done" && ( {props.summaryText && props.status === "done" && (
<div className="border-t border-green-800 bg-green-900/50 p-2"> <div className="border-t border-green-300 bg-green-100/50 p-2 dark:border-green-800 dark:bg-green-900/50">
<button <button
onClick={() => setShowSummary(!showSummary)} onClick={() => setShowSummary(!showSummary)}
className="flex items-center gap-1 text-xs font-normal text-green-400 hover:text-green-300" className="flex items-center gap-1 text-xs font-normal text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
> >
<FileText className="h-3 w-3" /> <FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"} {showSummary ? "Hide summary" : "Show summary"}
</button> </button>
{showSummary && ( {showSummary && (
<p className="mt-1 text-xs font-normal text-green-300"> <p className="mt-1 text-xs font-normal text-green-700 dark:text-green-300">
{props.summaryText} {props.summaryText}
</p> </p>
)} )}
@ -268,11 +272,11 @@ function formatDiff(diff: string) {
.split("\n") .split("\n")
.map((line) => { .map((line) => {
if (line.startsWith("+")) { if (line.startsWith("+")) {
return `<span class="text-green-400">${line}</span>`; return `<span class="text-green-600 dark:text-green-400">${line}</span>`;
} else if (line.startsWith("-")) { } else if (line.startsWith("-")) {
return `<span class="text-red-400">${line}</span>`; return `<span class="text-red-600 dark:text-red-400">${line}</span>`;
} else if (line.startsWith("@")) { } else if (line.startsWith("@")) {
return `<span class="text-blue-400">${line}</span>`; return `<span class="text-blue-600 dark:text-blue-400">${line}</span>`;
} }
return line; return line;
}) })

View file

@ -38,11 +38,11 @@ export function PlanViewer({
return ( return (
<div className={cn("w-full space-y-3", className)}> <div className={cn("w-full space-y-3", className)}>
<div className="mb-4 flex items-center gap-2"> <div className="mb-4 flex items-center gap-2">
<h3 className="text-sm font-medium text-white"> <h3 className="text-foreground text-sm font-medium">
{isProposedPlan ? "Proposed" : "Execution"} Plan {isProposedPlan ? "Proposed" : "Execution"} Plan
</h3> </h3>
{!isProposedPlan && ( {!isProposedPlan && (
<span className="text-xs text-gray-400"> <span className="text-muted-foreground text-xs">
{planItems.filter((item) => item.completed).length} of{" "} {planItems.filter((item) => item.completed).length} of{" "}
{planItems.length} completed {planItems.length} completed
</span> </span>
@ -61,47 +61,47 @@ export function PlanViewer({
className={cn( className={cn(
"flex items-start gap-3 rounded-lg border p-3 transition-colors", "flex items-start gap-3 rounded-lg border p-3 transition-colors",
{ {
"border-green-800 bg-green-900/30": status === "completed", "border-green-300 bg-green-100/30 dark:border-green-800 dark:bg-green-900/30":
"border-blue-800 bg-blue-900/30": status === "current", status === "completed",
"border-gray-800 bg-gray-900/30": [ "border-blue-300 bg-blue-100/30 dark:border-blue-800 dark:bg-blue-900/30":
"remaining", status === "current",
"proposed", "border-border bg-muted/30 dark:border-gray-800 dark:bg-gray-900/30":
].includes(status), ["remaining", "proposed"].includes(status),
}, },
)} )}
> >
{/* Status Icon */} {/* Status Icon */}
<div className="mt-0.5 flex-shrink-0"> <div className="mt-0.5 flex-shrink-0">
{status === "completed" && ( {status === "completed" && (
<Check className="h-4 w-4 text-green-400" /> <Check className="h-4 w-4 text-green-600 dark:text-green-400" />
)} )}
{status === "current" && ( {status === "current" && (
<Play className="h-4 w-4 text-blue-400" /> <Play className="h-4 w-4 text-blue-600 dark:text-blue-400" />
)} )}
{["remaining", "proposed"].includes(status) && ( {["remaining", "proposed"].includes(status) && (
<Clock className="h-4 w-4 text-gray-500" /> <Clock className="text-muted-foreground h-4 w-4" />
)} )}
</div> </div>
{/* Task Content */} {/* Task Content */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="mb-1 flex items-center justify-between text-gray-400"> <div className="text-muted-foreground mb-1 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-medium"> <span className="text-xs font-medium">
Step {item.index + 1} Step {item.index + 1}
</span> </span>
{status === "current" && ( {status === "current" && (
<span className="rounded-full bg-blue-900/50 px-2 py-0.5 text-xs text-blue-300"> <span className="rounded-full bg-blue-100/50 px-2 py-0.5 text-xs text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
In Progress In Progress
</span> </span>
)} )}
{status === "completed" && ( {status === "completed" && (
<span className="rounded-full bg-green-900/50 px-2 py-0.5 text-xs text-green-300"> <span className="rounded-full bg-green-100/50 px-2 py-0.5 text-xs text-green-700 dark:bg-green-900/50 dark:text-green-300">
Done Done
</span> </span>
)} )}
{status === "proposed" && ( {status === "proposed" && (
<span className="rounded-full bg-gray-800 px-2 py-0.5 text-xs text-gray-300"> <span className="bg-muted text-foreground/80 rounded-full px-2 py-0.5 text-xs dark:bg-gray-800 dark:text-gray-300">
Proposed Proposed
</span> </span>
)} )}
@ -162,13 +162,13 @@ export function PlanViewer({
}); });
}); });
}} }}
className="text-gray-200" className="text-foreground/90 dark:text-gray-200"
/> />
) : ( ) : (
<p <p
className={cn("text-sm leading-relaxed", { className={cn("text-sm leading-relaxed", {
"text-white": status === "current", "text-foreground": status === "current",
"text-gray-300": "text-foreground/80":
status === "completed" || status === "completed" ||
["remaining", "proposed"].includes(status), ["remaining", "proposed"].includes(status),
})} })}
@ -179,11 +179,13 @@ export function PlanViewer({
{/* Summary for completed tasks */} {/* Summary for completed tasks */}
{item.summary && status === "completed" && ( {item.summary && status === "completed" && (
<div className="mt-2 rounded border border-green-800 bg-green-900/30 p-2"> <div className="mt-2 rounded border border-green-300 bg-green-100/30 p-2 dark:border-green-800 dark:bg-green-900/30">
<p className="mb-1 text-xs font-medium text-green-300"> <p className="mb-1 text-xs font-medium text-green-700 dark:text-green-300">
Summary: Summary:
</p> </p>
<p className="text-xs text-green-400">{item.summary}</p> <p className="text-xs text-green-600 dark:text-green-400">
{item.summary}
</p>
</div> </div>
)} )}
</div> </div>
@ -193,7 +195,7 @@ export function PlanViewer({
{isProposedPlan && ( {isProposedPlan && (
<div <div
key="add-new-plan-item" key="add-new-plan-item"
className="flex items-start gap-3 rounded-lg border border-gray-800 bg-gray-900/30 p-3 transition-colors" className="border-border bg-muted/30 flex items-start gap-3 rounded-lg border p-3 transition-colors dark:border-gray-800 dark:bg-gray-900/30"
> >
<Textarea <Textarea
placeholder="Add new plan item" placeholder="Add new plan item"
@ -214,7 +216,7 @@ export function PlanViewer({
]); ]);
setNewPlanItem(""); setNewPlanItem("");
}} }}
className="mt-auto bg-inherit text-gray-200 transition-colors hover:bg-gray-800 hover:text-gray-100" className="text-foreground/90 hover:bg-muted hover:text-foreground mt-auto bg-inherit transition-colors dark:text-gray-200 dark:hover:bg-gray-800 dark:hover:text-gray-100"
size="sm" size="sm"
variant="outline" variant="outline"
> >

View file

@ -0,0 +1,82 @@
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
useEffect(() => {
const savedTheme = localStorage.getItem(storageKey) as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
}
}, [storageKey]);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider
{...props}
value={value}
>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};

View file

@ -0,0 +1,24 @@
"use client";
import { useTheme } from "@/components/theme-provider";
import { Moon, Sun } from "lucide-react";
import { TooltipIconButton } from "./ui/tooltip-icon-button";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<TooltipIconButton
tooltip="Toggle theme"
variant="ghost"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
aria-label="Toggle theme"
>
{theme === "light" ? (
<Moon className="size-4" />
) : (
<Sun className="size-4" />
)}
</TooltipIconButton>
);
}

View file

@ -24,6 +24,7 @@ import {
Settings, Settings,
FilePlus2, FilePlus2,
} from "lucide-react"; } from "lucide-react";
import { ThemeToggle } from "../theme-toggle";
import { useQueryState, parseAsBoolean, parseAsString } from "nuqs"; import { useQueryState, parseAsBoolean, parseAsString } from "nuqs";
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
import TaskListSidebar from "../task-list-sidebar"; import TaskListSidebar from "../task-list-sidebar";
@ -395,6 +396,7 @@ export function Thread() {
> >
<Settings className="size-4" /> <Settings className="size-4" />
</TooltipIconButton> </TooltipIconButton>
<ThemeToggle />
<GitHubOAuthButton /> <GitHubOAuthButton />
</div> </div>
</div> </div>

View file

@ -22,29 +22,29 @@ export function ToolCalls({
return ( return (
<div <div
key={idx} key={idx}
className="overflow-hidden rounded-lg border border-gray-700" className="border-border overflow-hidden rounded-lg border"
> >
<div className="border-b border-gray-700 bg-gray-800 px-4 py-2"> <div className="border-border bg-card border-b px-4 py-2 dark:bg-gray-800">
<h3 className="font-medium text-gray-100"> <h3 className="text-foreground font-medium">
{tc.name} {tc.name}
{tc.id && ( {tc.id && (
<code className="ml-2 rounded bg-gray-700 px-2 py-1 text-sm"> <code className="bg-muted ml-2 rounded px-2 py-1 text-sm dark:bg-gray-700">
{tc.id} {tc.id}
</code> </code>
)} )}
</h3> </h3>
</div> </div>
{hasArgs ? ( {hasArgs ? (
<table className="min-w-full divide-y divide-gray-700"> <table className="divide-border min-w-full divide-y">
<tbody className="divide-y divide-gray-700"> <tbody className="divide-border divide-y">
{Object.entries(args).map(([key, value], argIdx) => ( {Object.entries(args).map(([key, value], argIdx) => (
<tr key={argIdx}> <tr key={argIdx}>
<td className="px-4 py-2 text-sm font-medium whitespace-nowrap text-gray-100"> <td className="text-foreground px-4 py-2 text-sm font-medium whitespace-nowrap">
{key} {key}
</td> </td>
<td className="px-4 py-2 text-sm text-gray-300"> <td className="text-foreground/80 px-4 py-2 text-sm">
{isComplexValue(value) ? ( {isComplexValue(value) ? (
<code className="rounded bg-gray-800 px-2 py-1 font-mono text-sm break-all"> <code className="bg-muted/70 rounded px-2 py-1 font-mono text-sm break-all dark:bg-gray-800">
{JSON.stringify(value, null, 2)} {JSON.stringify(value, null, 2)}
</code> </code>
) : ( ) : (
@ -56,7 +56,9 @@ export function ToolCalls({
</tbody> </tbody>
</table> </table>
) : ( ) : (
<code className="block p-3 text-sm">{"{}"}</code> <code className="text-foreground/80 block p-3 text-sm">
{"{}"}
</code>
)} )}
</div> </div>
); );
@ -95,28 +97,28 @@ export function ToolResult({ message }: { message: ToolMessage }) {
return ( return (
<div className="mx-auto grid grid-rows-[1fr_auto] gap-2"> <div className="mx-auto grid grid-rows-[1fr_auto] gap-2">
<div className="overflow-hidden rounded-lg border border-gray-700"> <div className="border-border overflow-hidden rounded-lg border">
<div className="border-b border-gray-700 bg-gray-800 px-4 py-2"> <div className="border-border bg-card border-b px-4 py-2 dark:bg-gray-800">
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
{message.name ? ( {message.name ? (
<h3 className="font-medium text-gray-100"> <h3 className="text-foreground font-medium">
Tool Result:{" "} Tool Result:{" "}
<code className="rounded bg-gray-700 px-2 py-1"> <code className="bg-muted rounded px-2 py-1 dark:bg-gray-700">
{message.name} {message.name}
</code> </code>
</h3> </h3>
) : ( ) : (
<h3 className="font-medium text-gray-100">Tool Result</h3> <h3 className="text-foreground font-medium">Tool Result</h3>
)} )}
{message.tool_call_id && ( {message.tool_call_id && (
<code className="ml-2 rounded bg-gray-700 px-2 py-1 text-sm text-gray-100"> <code className="bg-muted text-foreground ml-2 rounded px-2 py-1 text-sm dark:bg-gray-700">
{message.tool_call_id} {message.tool_call_id}
</code> </code>
)} )}
</div> </div>
</div> </div>
<motion.div <motion.div
className="min-w-full bg-gray-700" className="bg-muted min-w-full dark:bg-gray-700"
initial={false} initial={false}
animate={{ height: "auto" }} animate={{ height: "auto" }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
@ -134,8 +136,8 @@ export function ToolResult({ message }: { message: ToolMessage }) {
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
{isJsonContent ? ( {isJsonContent ? (
<table className="min-w-full divide-y divide-gray-700"> <table className="divide-border min-w-full divide-y">
<tbody className="divide-y divide-gray-700"> <tbody className="divide-border divide-y">
{(Array.isArray(parsedContent) {(Array.isArray(parsedContent)
? isExpanded ? isExpanded
? parsedContent ? parsedContent
@ -147,12 +149,12 @@ export function ToolResult({ message }: { message: ToolMessage }) {
: [item[0], item[1]]; : [item[0], item[1]];
return ( return (
<tr key={argIdx}> <tr key={argIdx}>
<td className="px-4 py-2 text-sm font-medium whitespace-nowrap text-gray-100"> <td className="text-foreground px-4 py-2 text-sm font-medium whitespace-nowrap">
{key} {key}
</td> </td>
<td className="px-4 py-2 text-sm text-gray-300"> <td className="text-foreground/80 px-4 py-2 text-sm">
{isComplexValue(value) ? ( {isComplexValue(value) ? (
<code className="rounded bg-gray-800 px-2 py-1 font-mono text-sm break-all"> <code className="bg-muted/70 rounded px-2 py-1 font-mono text-sm break-all dark:bg-gray-800">
{JSON.stringify(value, null, 2)} {JSON.stringify(value, null, 2)}
</code> </code>
) : ( ) : (
@ -165,7 +167,7 @@ export function ToolResult({ message }: { message: ToolMessage }) {
</tbody> </tbody>
</table> </table>
) : ( ) : (
<code className="block text-sm text-gray-300"> <code className="text-foreground/80 block text-sm">
{displayedContent} {displayedContent}
</code> </code>
)} )}
@ -178,7 +180,7 @@ export function ToolResult({ message }: { message: ToolMessage }) {
parsedContent.length > 5)) && ( parsedContent.length > 5)) && (
<motion.button <motion.button
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
className="flex w-full cursor-pointer items-center justify-center border-t-[1px] border-gray-700 py-2 text-gray-300 transition-all duration-200 ease-in-out hover:bg-gray-800 hover:text-gray-200" className="border-border text-muted-foreground hover:bg-muted hover:text-foreground flex w-full cursor-pointer items-center justify-center border-t-[1px] py-2 transition-all duration-200 ease-in-out dark:hover:bg-gray-800 dark:hover:text-gray-200"
initial={{ scale: 1 }} initial={{ scale: 1 }}
whileHover={{ scale: 1.02 }} whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}

View file

@ -31,6 +31,9 @@ import {
} from "../ui/tooltip"; } from "../ui/tooltip";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import { ContentBlocksPreview } from "../thread/ContentBlocksPreview"; import { ContentBlocksPreview } from "../thread/ContentBlocksPreview";
import { TooltipIconButton } from "../ui/tooltip-icon-button";
import { ThemeToggle } from "../theme-toggle";
import { ThreadCard } from "./thread-card";
interface DefaultViewProps { interface DefaultViewProps {
threads: ThreadDisplayInfo[]; threads: ThreadDisplayInfo[];
@ -51,49 +54,6 @@ export function DefaultView({ threads }: DefaultViewProps) {
handlePaste, handlePaste,
} = useFileUpload(); } = useFileUpload();
const getStatusColor = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return "bg-blue-950 text-blue-400";
case "completed":
return "bg-green-950 text-green-400";
case "failed":
return "bg-red-950 text-red-400";
case "pending":
return "bg-yellow-950 text-yellow-400";
default:
return "bg-gray-800 text-gray-400";
}
};
const getStatusIcon = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return <Loader2 className="h-4 w-4 animate-spin" />;
case "completed":
return <CheckCircle className="h-4 w-4" />;
case "failed":
return <XCircle className="h-4 w-4" />;
default:
return null;
}
};
const getPRStatusColor = (status: string) => {
switch (status) {
case "merged":
return "text-purple-400";
case "open":
return "text-green-400";
case "draft":
return "text-gray-400";
case "closed":
return "text-red-400";
default:
return "text-gray-400";
}
};
if (!apiUrl || !assistantId) { if (!apiUrl || !assistantId) {
return <div>Missing API URL or Assistant ID</div>; return <div>Missing API URL or Assistant ID</div>;
} }
@ -101,15 +61,20 @@ export function DefaultView({ threads }: DefaultViewProps) {
return ( return (
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
{/* Header */} {/* Header */}
<div className="border-b border-gray-900 bg-black px-4 py-2"> <div className="border-border bg-card border-b px-4 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-2 w-2 rounded-full bg-green-500"></div> <div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="font-mono text-sm text-gray-400">Open SWE</span> <span className="text-muted-foreground font-mono text-sm">
Open SWE
</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-4">
<span className="text-xs text-gray-600">ready</span> <div className="flex items-center gap-2">
<div className="h-1 w-1 rounded-full bg-gray-600"></div> <span className="text-muted-foreground text-xs">ready</span>
<div className="bg-muted h-1 w-1 rounded-full"></div>
</div>
<ThemeToggle />
</div> </div>
</div> </div>
</div> </div>
@ -120,7 +85,7 @@ export function DefaultView({ threads }: DefaultViewProps) {
{/* Terminal Chat Input */} {/* Terminal Chat Input */}
<Card <Card
className={cn( className={cn(
"border-gray-800 bg-gray-950 py-0", "border-border bg-card py-0 dark:bg-gray-950",
dragOver dragOver
? "border-primary border-2 border-dotted" ? "border-primary border-2 border-dotted"
: "border border-solid", : "border border-solid",
@ -155,7 +120,7 @@ export function DefaultView({ threads }: DefaultViewProps) {
<TooltipTrigger> <TooltipTrigger>
<Label <Label
htmlFor="file-input" htmlFor="file-input"
className="flex cursor-pointer items-center justify-center rounded-full bg-inherit text-gray-500 hover:text-gray-300" className="text-muted-foreground hover:text-foreground flex cursor-pointer items-center justify-center rounded-full bg-inherit"
> >
<FilePlus2 className="size-4" /> <FilePlus2 className="size-4" />
</Label> </Label>
@ -171,13 +136,13 @@ export function DefaultView({ threads }: DefaultViewProps) {
{/* Recent & Running Threads */} {/* Recent & Running Threads */}
<div> <div>
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<h2 className="text-base font-semibold text-gray-300"> <h2 className="text-foreground text-base font-semibold">
Recent & Running Threads Recent & Running Threads
</h2> </h2>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-7 border-gray-700 bg-gray-900 text-xs text-gray-400 hover:bg-gray-800 hover:text-gray-300" className="border-border text-muted-foreground hover:text-foreground h-7 text-xs"
onClick={() => router.push("/chat/threads")} onClick={() => router.push("/chat/threads")}
> >
View All View All
@ -186,117 +151,46 @@ export function DefaultView({ threads }: DefaultViewProps) {
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
{threads.slice(0, 4).map((thread) => ( {threads.slice(0, 4).map((thread) => (
<Card <ThreadCard
key={thread.id} key={thread.id}
className="cursor-pointer border-gray-800 bg-gray-950 px-0 py-3 transition-shadow hover:bg-gray-900 hover:shadow-lg" thread={thread}
onClick={() => { />
router.push(`/chat/${thread.id}`);
}}
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="truncate text-sm font-medium text-gray-300">
{thread.title}
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="h-2 w-2 text-gray-600" />
<span className="truncate text-xs text-gray-500">
{thread.repository}
</span>
</div>
</div>
<Badge
variant="secondary"
className={`${getStatusColor(thread.status)} text-xs`}
>
<div className="flex items-center gap-1">
{getStatusIcon(thread.status)}
<span className="capitalize">{thread.status}</span>
</div>
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600">
{thread.taskCount === 0
? "No tasks"
: `${thread.taskCount} tasks`}
</span>
<span className="text-xs text-gray-600">•</span>
<span className="text-xs text-gray-600">
{thread.lastActivity}
</span>
</div>
<div className="flex items-center gap-1">
{thread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-gray-500 hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
window.open(thread.githubIssue!.url, "_blank");
}}
>
<Bug className="h-3 w-3" />
</Button>
)}
{thread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={`h-5 w-5 p-0 hover:text-gray-300 ${getPRStatusColor(thread.pullRequest.status)}`}
onClick={(e) => {
e.stopPropagation();
window.open(thread.pullRequest!.url, "_blank");
}}
>
<GitPullRequest className="h-3 w-3" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))} ))}
</div> </div>
</div> </div>
{/* Quick Actions */} {/* Quick Actions */}
<div> <div>
<h2 className="mb-3 text-base font-semibold text-gray-300"> <h2 className="text-foreground mb-3 text-base font-semibold">
Quick Actions Quick Actions
</h2> </h2>
<div className="grid gap-3 md:grid-cols-3"> <div className="grid gap-3 md:grid-cols-3">
<Card className="cursor-pointer border-gray-800 bg-gray-950 py-3 transition-shadow hover:bg-gray-900 hover:shadow-lg"> <Card className="border-border bg-card hover:bg-muted cursor-pointer py-3 transition-shadow hover:shadow-lg dark:bg-gray-950">
<CardHeader className="px-3"> <CardHeader className="px-3">
<CardTitle className="text-sm text-gray-300"> <CardTitle className="text-foreground text-sm">
Debug Code Debug Code
</CardTitle> </CardTitle>
<CardDescription className="text-xs text-gray-500"> <CardDescription className="text-muted-foreground text-xs">
Find and fix issues in your codebase Find and fix issues in your codebase
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
<Card className="cursor-pointer border-gray-800 bg-gray-950 py-3 transition-shadow hover:bg-gray-900 hover:shadow-lg"> <Card className="border-border bg-card hover:bg-muted cursor-pointer py-3 transition-shadow hover:shadow-lg dark:bg-gray-950">
<CardHeader className="px-3"> <CardHeader className="px-3">
<CardTitle className="text-sm text-gray-300"> <CardTitle className="text-foreground text-sm">
Add Feature Add Feature
</CardTitle> </CardTitle>
<CardDescription className="text-xs text-gray-500"> <CardDescription className="text-muted-foreground text-xs">
Implement new functionality Implement new functionality
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
<Card className="cursor-pointer border-gray-800 bg-gray-950 py-3 transition-shadow hover:bg-gray-900 hover:shadow-lg"> <Card className="border-border bg-card hover:bg-muted cursor-pointer py-3 transition-shadow hover:shadow-lg dark:bg-gray-950">
<CardHeader className="px-3"> <CardHeader className="px-3">
<CardTitle className="text-sm text-gray-300"> <CardTitle className="text-foreground text-sm">
Refactor Code Refactor Code
</CardTitle> </CardTitle>
<CardDescription className="text-xs text-gray-500"> <CardDescription className="text-muted-foreground text-xs">
Improve code structure and performance Improve code structure and performance
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>

View file

@ -106,19 +106,19 @@ export function TerminalInput({
}; };
return ( return (
<div className="rounded-md border border-gray-600 bg-black p-2 font-mono text-xs"> <div className="border-border bg-muted rounded-md border p-2 font-mono text-xs dark:bg-black">
<div className="flex items-start gap-1 text-gray-300"> <div className="text-foreground flex items-start gap-1">
{/* User@Host */} {/* User@Host */}
<span className="text-gray-400">open-swe</span> <span className="text-muted-foreground">open-swe</span>
<span className="text-gray-500">@</span> <span className="text-muted-foreground/70">@</span>
<span className="text-gray-400">github</span> <span className="text-muted-foreground">github</span>
<span className="text-gray-500">:</span> <span className="text-muted-foreground/70">:</span>
{/* Repository & Branch Selectors */} {/* Repository & Branch Selectors */}
<RepositoryBranchSelectors /> <RepositoryBranchSelectors />
{/* Prompt */} {/* Prompt */}
<span className="text-gray-400">$</span> <span className="text-muted-foreground">$</span>
</div> </div>
{/* Multiline Input */} {/* Multiline Input */}
@ -129,7 +129,7 @@ export function TerminalInput({
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder={placeholder} placeholder={placeholder}
disabled={disabled} disabled={disabled}
className="min-h-[40px] flex-1 resize-none border-none bg-transparent p-0 font-mono text-xs text-white placeholder:text-gray-600 focus-visible:ring-0 focus-visible:ring-offset-0" className="text-foreground placeholder:text-muted-foreground min-h-[40px] flex-1 resize-none border-none bg-transparent p-0 font-mono text-xs focus-visible:ring-0 focus-visible:ring-offset-0"
rows={3} rows={3}
onPaste={onPaste} onPaste={onPaste}
/> />
@ -137,7 +137,7 @@ export function TerminalInput({
onClick={handleSend} onClick={handleSend}
disabled={disabled || !message.trim()} disabled={disabled || !message.trim()}
size="sm" size="sm"
className="h-7 w-7 self-end bg-gray-700 p-0 hover:bg-gray-600" 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"
> >
{loading ? ( {loading ? (
<Loader2 className="size-3 animate-spin" /> <Loader2 className="size-3 animate-spin" />
@ -148,7 +148,9 @@ export function TerminalInput({
</div> </div>
{/* Help text */} {/* Help text */}
<div className="mt-1 text-xs text-gray-600">Press Cmd+Enter to send</div> <div className="text-muted-foreground mt-1 text-xs">
Press Cmd+Enter to send
</div>
</div> </div>
); );
} }

View file

@ -0,0 +1,138 @@
import {
Bug,
CheckCircle,
GitBranch,
GitPullRequest,
Loader2,
XCircle,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
import { ThreadDisplayInfo } from "./types";
import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
const router = useRouter();
const getStatusColor = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return "dark:bg-blue-950 bg-blue-100 dark:text-blue-400 text-blue-700";
case "completed":
return "dark:bg-green-950 bg-green-100 dark:text-green-400 text-green-700";
case "failed":
return "dark:bg-red-950 bg-red-100 dark:text-red-400 text-red-700";
case "pending":
return "dark:bg-yellow-950 bg-yellow-100 dark:text-yellow-400 text-yellow-700";
default:
return "dark:bg-gray-800 bg-gray-200 dark:text-gray-400 text-gray-700";
}
};
const getStatusIcon = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return <Loader2 className="h-4 w-4 animate-spin" />;
case "completed":
return <CheckCircle className="h-4 w-4" />;
case "failed":
return <XCircle className="h-4 w-4" />;
default:
return null;
}
};
const getPRStatusColor = (status: string) => {
switch (status) {
case "merged":
return "dark:text-purple-400 text-purple-600";
case "open":
return "dark:text-green-400 text-green-600";
case "draft":
return "dark:text-gray-400 text-gray-600";
case "closed":
return "dark:text-red-400 text-red-600";
default:
return "dark:text-gray-400 text-gray-600";
}
};
return (
<Card
key={thread.id}
className="border-border bg-card hover:bg-muted cursor-pointer px-0 py-3 transition-shadow hover:shadow-lg dark:bg-gray-950"
onClick={() => {
router.push(`/chat/${thread.id}`);
}}
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<CardTitle className="text-foreground truncate text-sm font-medium">
{thread.title}
</CardTitle>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="text-muted-foreground h-2 w-2" />
<span className="text-muted-foreground truncate text-xs">
{thread.repository}
</span>
</div>
</div>
<Badge
variant="secondary"
className={`${getStatusColor(thread.status)} text-xs`}
>
<div className="flex items-center gap-1">
{getStatusIcon(thread.status)}
<span className="capitalize">{thread.status}</span>
</div>
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
{thread.taskCount === 0
? "No tasks"
: `${thread.taskCount} tasks`}
</span>
<span className="text-muted-foreground text-xs">•</span>
<span className="text-muted-foreground text-xs">
{thread.lastActivity}
</span>
</div>
<div className="flex items-center gap-1">
{thread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground h-5 w-5 p-0"
onClick={(e) => {
e.stopPropagation();
window.open(thread.githubIssue!.url, "_blank");
}}
>
<Bug className="h-3 w-3" />
</Button>
)}
{thread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={`h-5 w-5 p-0 hover:text-gray-300 ${getPRStatusColor(thread.pullRequest.status)}`}
onClick={(e) => {
e.stopPropagation();
window.open(thread.pullRequest!.url, "_blank");
}}
>
<GitPullRequest className="h-3 w-3" />
</Button>
)}
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -21,67 +21,21 @@ import {
Bug, Bug,
} from "lucide-react"; } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { getThreadTitle } from "@/lib/thread";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { ThreadDisplayInfo } from "./types"; import { ThreadDisplayInfo } from "./types";
import { ThreadCard } from "./thread-card";
interface ThreadSwitcherProps { interface ThreadSwitcherProps {
currentThread: ThreadDisplayInfo; currentThread: ThreadDisplayInfo;
allThreads: ThreadDisplayInfo[]; allThreads: ThreadDisplayInfo[];
onThreadSelect: (thread: ThreadDisplayInfo) => void;
onNewChat: () => void;
} }
export function ThreadSwitcher({ export function ThreadSwitcher({
currentThread, currentThread,
allThreads, allThreads,
onThreadSelect,
onNewChat,
}: ThreadSwitcherProps) { }: ThreadSwitcherProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const getStatusIcon = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return <Loader2 className="h-3 w-3 animate-spin text-blue-400" />;
case "completed":
return <CheckCircle className="h-3 w-3 text-green-400" />;
case "failed":
return <XCircle className="h-3 w-3 text-red-400" />;
default:
return <div className="h-3 w-3 rounded-full bg-gray-700" />;
}
};
const getStatusColor = (status: ThreadDisplayInfo["status"]) => {
switch (status) {
case "running":
return "bg-blue-950 text-blue-400";
case "completed":
return "bg-green-950 text-green-400";
case "failed":
return "bg-red-950 text-red-400";
default:
return "bg-gray-800 text-gray-400";
}
};
const getPRStatusColor = (status: string) => {
switch (status) {
case "merged":
return "text-purple-400";
case "open":
return "text-green-400";
case "draft":
return "text-gray-400";
case "closed":
return "text-red-400";
default:
return "text-gray-400";
}
};
const otherThreads = allThreads.filter((t) => t.id !== currentThread.id); const otherThreads = allThreads.filter((t) => t.id !== currentThread.id);
const runningCount = otherThreads.filter( const runningCount = otherThreads.filter(
(t) => t.status === "running", (t) => t.status === "running",
@ -96,14 +50,14 @@ export function ThreadSwitcher({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-7 gap-1 border-gray-700 bg-gray-900 text-xs text-gray-400 hover:bg-gray-800 hover:text-gray-300" className="border-border bg-card text-muted-foreground hover:bg-muted hover:text-foreground h-7 gap-1 text-xs dark:border-gray-700 dark:bg-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-300"
> >
<Layers3 className="h-3 w-3" /> <Layers3 className="h-3 w-3" />
<span className="hidden sm:inline">Switch Thread</span> <span className="hidden sm:inline">Switch Thread</span>
{runningCount > 0 && ( {runningCount > 0 && (
<Badge <Badge
variant="secondary" variant="secondary"
className="h-4 bg-blue-950 px-1 text-xs text-blue-400" className="h-4 bg-blue-100 px-1 text-xs text-blue-700 dark:bg-blue-950 dark:text-blue-400"
> >
{runningCount} {runningCount}
</Badge> </Badge>
@ -112,22 +66,22 @@ export function ThreadSwitcher({
</SheetTrigger> </SheetTrigger>
<SheetContent <SheetContent
side="right" side="right"
className="w-80 border-gray-800 bg-gray-950 sm:w-96" className="border-border bg-background w-80 sm:w-96 dark:border-gray-800 dark:bg-gray-950"
> >
<SheetHeader className="pb-4"> <SheetHeader className="pb-4">
<SheetTitle className="text-base text-gray-300"> <SheetTitle className="text-foreground text-base">
All Threads All Threads
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<div className="space-y-3"> <div className="mx-2 h-full space-y-3">
{/* New Chat Button */} {/* New Chat Button */}
<Button <Button
onClick={() => { onClick={() => {
router.push("/chat"); router.push("/chat");
setOpen(false); setOpen(false);
}} }}
className="h-8 w-full justify-start gap-2 border-gray-700 bg-gray-900 text-xs text-gray-300 hover:bg-gray-800" className="border-border bg-card text-foreground hover:bg-muted h-8 w-full justify-start gap-2 text-xs dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800"
variant="outline" variant="outline"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
@ -136,146 +90,25 @@ export function ThreadSwitcher({
{/* Current Thread */} {/* Current Thread */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-xs font-medium tracking-wide text-gray-500 uppercase"> <h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase dark:text-gray-500">
Current Thread Current Thread
</h3> </h3>
<div className="rounded-lg border-2 border-blue-800 bg-blue-950 p-3"> <ThreadCard thread={currentThread} />
<div className="flex items-start gap-2">
{getStatusIcon(currentThread.status)}
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-gray-300">
{currentThread.title}
</div>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="h-2 w-2 text-gray-600" />
<span className="truncate text-xs text-gray-500">
{currentThread.repository}
</span>
</div>
<div className="mt-2 flex items-center justify-between">
<Badge
variant="secondary"
className={`${getStatusColor(currentThread.status)} text-xs`}
>
{currentThread.status}
</Badge>
<div className="flex items-center gap-1">
{currentThread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 text-gray-500 hover:text-gray-300"
onClick={(e) => {
e.stopPropagation();
// TODO: Open issue in GitHub
alert("Open issue in github not implemented.");
}}
>
<Bug className="h-3 w-3" />
</Button>
)}
{/* TODO: Add PR info to state, then hook this up. */}
{/* {currentThread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={`h-5 w-5 p-0 hover:text-gray-300 ${getPRStatusColor(currentThread.pullRequest.status)}`}
onClick={(e) => {
e.stopPropagation()
window.open(currentThread.pullRequest!.url, "_blank")
}}
>
<GitPullRequest className="h-3 w-3" />
</Button>
)} */}
</div>
</div>
</div>
</div>
</div>
</div> </div>
{/* Other Threads */} {/* Other Threads */}
{otherThreads.length > 0 && ( {otherThreads.length > 0 && (
<div className="space-y-2"> <div className="h-full space-y-2">
<h3 className="text-xs font-medium tracking-wide text-gray-500 uppercase"> <h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase dark:text-gray-500">
Other Threads Other Threads
</h3> </h3>
<ScrollArea className="h-96"> <ScrollArea className="h-full">
<div className="space-y-1"> <div className="space-y-1">
{otherThreads.map((thread) => ( {otherThreads.map((thread) => (
<Button <ThreadCard
thread={thread}
key={thread.id} key={thread.id}
variant="ghost" />
className="h-auto w-full justify-start p-3 text-left text-gray-400 hover:bg-gray-800"
onClick={() => {
router.push(`/chat/${thread.id}`);
setOpen(false);
}}
>
<div className="flex w-full items-start gap-2">
{getStatusIcon(thread.status)}
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-medium text-gray-300">
{thread.title}
</div>
<div className="mt-1 flex items-center gap-1">
<GitBranch className="h-2 w-2 text-gray-600" />
<span className="truncate text-xs text-gray-500">
{thread.repository}
</span>
</div>
<div className="mt-1 flex items-center justify-between">
{thread.taskCount && (
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600">
{thread.taskCount} tasks
</span>
<Badge
variant="secondary"
className={`${getStatusColor(thread.status)} text-xs`}
>
{thread.status}
</Badge>
</div>
)}
<div className="flex items-center gap-1">
{thread.githubIssue && (
<Button
variant="ghost"
size="sm"
className="h-4 w-4 p-0 text-gray-600 hover:text-gray-400"
onClick={(e) => {
e.stopPropagation();
// TODO: Open issue in GitHub
alert(
"Open issue in github not implemented.",
);
}}
>
<Bug className="h-2 w-2" />
</Button>
)}
{/* TODO: Add PR info to state, then hook this up. */}
{/* {thread.pullRequest && (
<Button
variant="ghost"
size="sm"
className={`h-4 w-4 p-0 hover:text-gray-400 ${getPRStatusColor(thread.pullRequest.status)}`}
onClick={(e) => {
e.stopPropagation()
window.open(thread.pullRequest!.url, "_blank")
}}
>
<GitPullRequest className="h-2 w-2" />
</Button>
)} */}
</div>
</div>
</div>
</div>
</Button>
))} ))}
</div> </div>
</ScrollArea> </ScrollArea>

View file

@ -12,13 +12,15 @@ const isEven = (num: number) => num % 2 === 0;
export function LoadingActionsSection() { export function LoadingActionsSection() {
return ( return (
<Card className="border-gray-800 bg-gray-950"> <Card className="border-border bg-card dark:bg-gray-950">
<CardHeader className="p-3"> <CardHeader className="p-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-32 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-4 w-32 animate-pulse rounded dark:bg-gray-700"></div>
<div className="ml-auto flex items-center gap-1"> <div className="ml-auto flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin text-gray-500" /> <Loader2 className="text-muted-foreground h-3 w-3 animate-spin" />
<span className="text-xs text-gray-500">Loading actions...</span> <span className="text-muted-foreground text-xs">
Loading actions...
</span>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@ -26,35 +28,35 @@ export function LoadingActionsSection() {
{Array.from({ length: 8 }, (_, i) => ( {Array.from({ length: 8 }, (_, i) => (
<div <div
key={i} key={i}
className="space-y-2 rounded-lg border border-gray-800 bg-gray-900 p-3" className="border-border bg-muted/30 space-y-2 rounded-lg border p-3 dark:bg-gray-900"
> >
{isEven(i) ? ( {isEven(i) ? (
<div className="space-y-1 rounded bg-black p-2"> <div className="bg-background space-y-1 rounded p-2 dark:bg-black">
<div className="h-2 w-32 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-2 w-32 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-2 w-48 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-2 w-48 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
) : ( ) : (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-4 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-3 w-40 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-40 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-3 w-16 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-16 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-4 w-4 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-4 w-4 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
</div> </div>
)} )}
</div> </div>
))} ))}
<div className="space-y-2 rounded-lg border-2 border-dashed border-gray-700 p-3"> <div className="border-border space-y-2 rounded-lg border-2 border-dashed p-3">
<div className="h-12 animate-pulse rounded bg-gray-800"></div> <div className="bg-muted h-12 animate-pulse rounded dark:bg-gray-800"></div>
<div className="h-7 w-16 animate-pulse rounded bg-gray-800"></div> <div className="bg-muted h-7 w-16 animate-pulse rounded dark:bg-gray-800"></div>
</div> </div>
<div className="flex gap-2 pt-3"> <div className="flex gap-2 pt-3">
<div className="h-8 flex-1 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-8 flex-1 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-8 flex-1 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -65,7 +67,7 @@ export function LoadingChatSection() {
return ( return (
<div className="flex-1 space-y-4 overflow-y-auto p-4"> <div className="flex-1 space-y-4 overflow-y-auto p-4">
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<div className="flex items-center gap-2 text-gray-500"> <div className="text-muted-foreground flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Loading conversation...</span> <span className="text-sm">Loading conversation...</span>
</div> </div>
@ -77,18 +79,18 @@ export function LoadingChatSection() {
className="flex gap-3" className="flex gap-3"
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div className="h-6 w-6 animate-pulse rounded-full bg-gray-700"></div> <div className="bg-muted h-6 w-6 animate-pulse rounded-full dark:bg-gray-700"></div>
</div> </div>
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-3 w-16 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-16 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-3 w-12 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-12 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="h-3 w-full animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-full animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-3 w-3/4 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-3/4 animate-pulse rounded dark:bg-gray-700"></div>
{i === 1 && ( {i === 1 && (
<div className="h-3 w-1/2 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-1/2 animate-pulse rounded dark:bg-gray-700"></div>
)} )}
</div> </div>
</div> </div>
@ -100,42 +102,42 @@ export function LoadingChatSection() {
export function ThreadViewLoading({ onBackToHome }: ThreadViewLoadingProps) { export function ThreadViewLoading({ onBackToHome }: ThreadViewLoadingProps) {
return ( return (
<div className="flex h-screen flex-1 flex-col bg-black"> <div className="bg-background flex h-screen flex-1 flex-col">
<div className="absolute top-0 right-0 left-0 z-10 border-b border-gray-900 bg-black px-4 py-2"> <div className="border-border bg-card absolute top-0 right-0 left-0 z-10 border-b px-4 py-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 text-gray-600 hover:bg-gray-900 hover:text-gray-400" className="text-muted-foreground hover:bg-muted hover:text-foreground h-6 w-6 p-0"
onClick={onBackToHome} onClick={onBackToHome}
> >
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
</Button> </Button>
<div className="flex min-w-0 flex-1 items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-2">
<div className="h-2 w-2 animate-pulse rounded-full bg-gray-700"></div> <div className="bg-muted h-2 w-2 animate-pulse rounded-full dark:bg-gray-700"></div>
<div className="h-3 w-48 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-48 animate-pulse rounded dark:bg-gray-700"></div>
<span className="text-xs text-gray-600">•</span> <span className="text-muted-foreground text-xs">•</span>
<div className="h-3 w-24 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-24 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
<div className="h-7 w-28 animate-pulse rounded bg-gray-800"></div> <div className="bg-muted/70 h-7 w-28 animate-pulse rounded dark:bg-gray-800"></div>
</div> </div>
</div> </div>
<div className="flex h-full w-full pt-12"> <div className="flex h-full w-full pt-12">
<div className="flex h-full w-1/3 flex-col border-r border-gray-900 bg-gray-950"> <div className="border-border bg-muted/30 flex h-full w-1/3 flex-col border-r dark:bg-gray-950">
<LoadingChatSection /> <LoadingChatSection />
<div className="border-t border-gray-800 bg-gray-950 p-4"> <div className="border-border bg-muted/30 border-t p-4 dark:bg-gray-950">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="flex-1 rounded-md border border-gray-700 bg-gray-900 p-3"> <div className="border-border bg-background flex-1 rounded-md border p-3 dark:bg-gray-900">
<div className="space-y-2"> <div className="space-y-2">
<div className="h-3 w-1/3 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-1/3 animate-pulse rounded dark:bg-gray-700"></div>
<div className="h-3 w-1/2 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted h-3 w-1/2 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
</div> </div>
<div className="h-10 w-10 animate-pulse self-end rounded bg-gray-700"></div> <div className="bg-muted h-10 w-10 animate-pulse self-end rounded dark:bg-gray-700"></div>
</div> </div>
<div className="mt-2 h-3 w-32 animate-pulse rounded bg-gray-700"></div> <div className="bg-muted mt-2 h-3 w-32 animate-pulse rounded dark:bg-gray-700"></div>
</div> </div>
</div> </div>

View file

@ -13,6 +13,7 @@ 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 { ActionsRenderer } from "./actions-renderer"; import { ActionsRenderer } from "./actions-renderer";
import { ThemeToggle } from "../theme-toggle";
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;
@ -21,7 +22,6 @@ interface ThreadViewProps {
stream: ReturnType<typeof useStream<ManagerGraphState>>; stream: ReturnType<typeof useStream<ManagerGraphState>>;
displayThread: ThreadDisplayInfo; displayThread: ThreadDisplayInfo;
allDisplayThreads: ThreadDisplayInfo[]; allDisplayThreads: ThreadDisplayInfo[];
onThreadSelect: (thread: ThreadDisplayInfo) => void;
onBackToHome: () => void; onBackToHome: () => void;
} }
@ -29,7 +29,6 @@ export function ThreadView({
stream, stream,
displayThread, displayThread,
allDisplayThreads, allDisplayThreads,
onThreadSelect,
onBackToHome, onBackToHome,
}: ThreadViewProps) { }: ThreadViewProps) {
const [chatInput, setChatInput] = useState(""); const [chatInput, setChatInput] = useState("");
@ -50,14 +49,14 @@ export function ThreadView({
}; };
return ( return (
<div className="flex h-screen flex-1 flex-col bg-black"> <div className="bg-background flex h-screen flex-1 flex-col">
{/* Header */} {/* Header */}
<div className="absolute top-0 right-0 left-0 z-10 border-b border-gray-900 bg-black px-4 py-2"> <div className="border-border bg-card absolute top-0 right-0 left-0 z-10 border-b px-4 py-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 w-6 p-0 text-gray-600 hover:bg-gray-900 hover:text-gray-400" className="text-muted-foreground hover:bg-muted hover:text-foreground h-6 w-6 p-0"
onClick={onBackToHome} onClick={onBackToHome}
> >
<ArrowLeft className="h-3 w-3" /> <ArrowLeft className="h-3 w-3" />
@ -72,28 +71,27 @@ export function ThreadView({
: "bg-red-500" : "bg-red-500"
}`} }`}
></div> ></div>
<span className="truncate font-mono text-sm text-gray-400"> <span className="text-muted-foreground truncate font-mono text-sm">
{displayThread.title} {displayThread.title}
</span> </span>
<span className="text-xs text-gray-600">•</span> <span className="text-muted-foreground text-xs">•</span>
<GitBranch className="h-3 w-3 text-gray-600" /> <GitBranch className="text-muted-foreground h-3 w-3" />
<span className="truncate text-xs text-gray-600"> <span className="text-muted-foreground truncate text-xs">
{displayThread.repository} {displayThread.repository}
</span> </span>
</div> </div>
<ThreadSwitcher <ThreadSwitcher
currentThread={displayThread} currentThread={displayThread}
allThreads={allDisplayThreads} allThreads={allDisplayThreads}
onThreadSelect={onThreadSelect}
onNewChat={onBackToHome}
/> />
<ThemeToggle />
</div> </div>
</div> </div>
{/* Main Content - Split Layout */} {/* Main Content - Split Layout */}
<div className="flex h-full w-full pt-12"> <div className="flex h-full w-full pt-12">
{/* Left Side - Chat Interface */} {/* Left Side - Chat Interface */}
<div className="flex h-full w-1/3 flex-col border-r border-gray-900 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="flex-1 space-y-4 overflow-y-auto p-4">
{stream.messages.map((message) => ( {stream.messages.map((message) => (
@ -103,22 +101,22 @@ export function ThreadView({
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{message.type === "human" ? ( {message.type === "human" ? (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-gray-700"> <div className="bg-muted flex h-6 w-6 items-center justify-center rounded-full dark:bg-gray-700">
<User className="h-3 w-3 text-gray-400" /> <User className="text-muted-foreground h-3 w-3" />
</div> </div>
) : ( ) : (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-900"> <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-400" /> <Bot className="h-3 w-3 text-blue-700 dark:text-blue-400" />
</div> </div>
)} )}
</div> </div>
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-400"> <span className="text-muted-foreground text-xs font-medium">
{message.type === "human" ? "You" : "AI Agent"} {message.type === "human" ? "You" : "Agent"}
</span> </span>
</div> </div>
<div className="text-sm leading-relaxed text-gray-300"> <div className="text-foreground text-sm leading-relaxed">
{getMessageContentString(message.content)} {getMessageContentString(message.content)}
</div> </div>
</div> </div>
@ -127,13 +125,13 @@ export function ThreadView({
</div> </div>
{/* Chat Input - Fixed at bottom */} {/* Chat Input - Fixed at bottom */}
<div className="border-t border-gray-800 bg-gray-950 p-4"> <div className="border-border bg-muted/30 border-t p-4 dark:bg-gray-950">
<div className="flex gap-2"> <div className="flex gap-2">
<Textarea <Textarea
value={chatInput} value={chatInput}
onChange={(e) => setChatInput(e.target.value)} onChange={(e) => setChatInput(e.target.value)}
placeholder="Type your message..." placeholder="Type your message..."
className="min-h-[60px] flex-1 resize-none border-gray-700 bg-gray-900 text-sm text-gray-300 placeholder:text-gray-600" className="border-border bg-background text-foreground placeholder:text-muted-foreground min-h-[60px] flex-1 resize-none text-sm dark:bg-gray-900"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
@ -145,12 +143,12 @@ export function ThreadView({
onClick={handleSendMessage} onClick={handleSendMessage}
disabled={!chatInput.trim()} disabled={!chatInput.trim()}
size="sm" size="sm"
className="h-10 w-10 self-end bg-gray-700 p-0 hover:bg-gray-600" className="bg-muted hover:bg-muted/80 h-10 w-10 self-end p-0 dark:bg-gray-700 hover:dark:bg-gray-600"
> >
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
</Button> </Button>
</div> </div>
<div className="mt-2 text-xs text-gray-600"> <div className="text-muted-foreground mt-2 text-xs">
Press Cmd+Enter to send Press Cmd+Enter to send
</div> </div>
</div> </div>
@ -163,17 +161,12 @@ export function ThreadView({
defaultValue="planner" defaultValue="planner"
className="w-full" className="w-full"
> >
<TabsList> <TabsList className="bg-muted/70 dark:bg-gray-800">
<TabsTrigger value="planner">Planner</TabsTrigger> <TabsTrigger value="planner">Planner</TabsTrigger>
<TabsTrigger value="programmer">Programmer</TabsTrigger> <TabsTrigger value="programmer">Programmer</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="planner"> <TabsContent value="planner">
<Card className="border-gray-800 bg-gray-950 px-0 py-4"> <Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardHeader>
<CardTitle className="text-base text-gray-300">
Planning Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 p-3 pt-0"> <CardContent className="space-y-2 p-3 pt-0">
{plannerThreadId && {plannerThreadId &&
plannerRunId && plannerRunId &&
@ -190,12 +183,7 @@ export function ThreadView({
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="programmer"> <TabsContent value="programmer">
<Card className="border-gray-800 bg-gray-950 px-0 py-4"> <Card className="border-border bg-card px-0 py-4 dark:bg-gray-950">
<CardHeader>
<CardTitle className="text-base text-gray-300">
Code Actions
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 p-3 pt-0"> <CardContent className="space-y-2 p-3 pt-0">
{programmerSession && PROGRAMMER_ASSISTANT_ID && ( {programmerSession && PROGRAMMER_ASSISTANT_ID && (
<ActionsRenderer<PlannerGraphState> <ActionsRenderer<PlannerGraphState>

View file

@ -1,6 +1,9 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], darkMode: "class",
future: {
hoverOnlyWhenSupported: true,
},
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{ts,tsx,js,jsx}", "./src/**/*.{ts,tsx,js,jsx}",